package auth

import (
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"time"

	"github.com/redis/go-redis/v9"

	"github.com/wangjia/pangolin/server/internal/apierr"
	"github.com/wangjia/pangolin/server/internal/totp"
)

const totpIssuer = "Pangolin"

// errTOTPInvalid mirrors the web user-center's expected error code.
var errTOTPInvalid = apierr.New("totp_invalid", "动态码不正确，请重试", "Invalid code, try again")

// TOTPHandler serves the user two-factor endpoints (/v1/me/totp/*) and the
// second login step (/v1/auth/login/totp). Secrets are stored AES-256-GCM
// encrypted under encKey; pending login tokens live in Redis.
type TOTPHandler struct {
	db     *sql.DB
	encKey []byte // 32 bytes (AES-256)
	tokens *TokenManager
	rdb    *redis.Client
	now    func() time.Time
}

// NewTOTPHandler wires the handler. encKey must be 32 bytes.
func NewTOTPHandler(db *sql.DB, encKey []byte, tokens *TokenManager, rdb *redis.Client) *TOTPHandler {
	return &TOTPHandler{db: db, encKey: encKey, tokens: tokens, rdb: rdb, now: time.Now}
}

type totpCodeRequest struct {
	Code string `json:"code"`
}

// Setup handles POST /v1/me/totp/setup — generates and stores a secret (not yet
// enabled) and returns it plus an otpauth:// URI for QR rendering.
func (h *TOTPHandler) Setup(w http.ResponseWriter, r *http.Request) {
	uid, ok := UserIDFromContext(r.Context())
	if !ok {
		apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
		return
	}
	var email string
	if err := h.db.QueryRowContext(r.Context(),
		`SELECT email FROM users WHERE id = ? AND status = 'active'`, uid).Scan(&email); err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	secret, err := totp.GenerateSecret()
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	enc, err := sealAES(h.encKey, secret)
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	if _, err := h.db.ExecContext(r.Context(),
		`UPDATE users SET totp_secret_enc = ?, totp_enabled = FALSE WHERE id = ?`, enc, uid); err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	_ = json.NewEncoder(w).Encode(map[string]string{
		"secret":      secret,
		"otpauth_uri": totp.ProvisioningURI(secret, email, totpIssuer),
	})
}

// Verify handles POST /v1/me/totp/verify {code} — enables TOTP after confirming
// the user can produce a valid code for the secret created by Setup.
func (h *TOTPHandler) Verify(w http.ResponseWriter, r *http.Request) {
	uid, ok := UserIDFromContext(r.Context())
	if !ok {
		apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
		return
	}
	var req totpCodeRequest
	if !decodeJSON(w, r, &req) {
		return
	}
	secret, _, err := h.loadSecret(r.Context(), uid)
	if err != nil || secret == "" || !totp.Validate(secret, req.Code, h.now().UTC(), 1) {
		apierr.WriteJSON(w, http.StatusUnauthorized, errTOTPInvalid)
		return
	}
	if _, err := h.db.ExecContext(r.Context(),
		`UPDATE users SET totp_enabled = TRUE WHERE id = ?`, uid); err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// Disable handles POST /v1/me/totp/disable {code} — turns off TOTP after a valid
// code and clears the stored secret.
func (h *TOTPHandler) Disable(w http.ResponseWriter, r *http.Request) {
	uid, ok := UserIDFromContext(r.Context())
	if !ok {
		apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized)
		return
	}
	var req totpCodeRequest
	if !decodeJSON(w, r, &req) {
		return
	}
	secret, _, err := h.loadSecret(r.Context(), uid)
	if err != nil || secret == "" || !totp.Validate(secret, req.Code, h.now().UTC(), 1) {
		apierr.WriteJSON(w, http.StatusUnauthorized, errTOTPInvalid)
		return
	}
	if _, err := h.db.ExecContext(r.Context(),
		`UPDATE users SET totp_secret_enc = NULL, totp_enabled = FALSE WHERE id = ?`, uid); err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

type loginTOTPRequest struct {
	PendingToken string `json:"pending_token"`
	Code         string `json:"code"`
}

// LoginTOTP handles POST /v1/auth/login/totp — second login step. Consumes the
// one-time pending token from Login, validates the TOTP code, then issues tokens.
func (h *TOTPHandler) LoginTOTP(w http.ResponseWriter, r *http.Request) {
	var req loginTOTPRequest
	if !decodeJSON(w, r, &req) {
		return
	}
	if req.PendingToken == "" {
		apierr.WriteJSON(w, http.StatusUnauthorized, errTOTPInvalid)
		return
	}
	// One-time consume of the pending token.
	uidStr, err := h.rdb.GetDel(r.Context(), totpPendingPrefix+req.PendingToken).Result()
	if errors.Is(err, redis.Nil) {
		apierr.WriteJSON(w, http.StatusUnauthorized, errTOTPInvalid)
		return
	} else if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	uid, err := strconv.ParseInt(uidStr, 10, 64)
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}

	secret, enabled, err := h.loadSecret(r.Context(), uid)
	if err != nil || !enabled || secret == "" || !totp.Validate(secret, req.Code, h.now().UTC(), 1) {
		apierr.WriteJSON(w, http.StatusUnauthorized, errTOTPInvalid)
		return
	}

	var uuid string
	if err := h.db.QueryRowContext(r.Context(),
		`SELECT uuid FROM users WHERE id = ? AND status = 'active'`, uid).Scan(&uuid); err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	pair, err := h.tokens.Issue(r.Context(), uid, uuid)
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	writeTokenPair(w, pair)
}

// loadSecret returns the user's decrypted TOTP secret and enabled flag. A NULL
// secret yields ("", enabled, nil).
func (h *TOTPHandler) loadSecret(ctx context.Context, uid int64) (string, bool, error) {
	var enc []byte
	var enabled bool
	if err := h.db.QueryRowContext(ctx,
		`SELECT totp_secret_enc, totp_enabled FROM users WHERE id = ?`, uid).Scan(&enc, &enabled); err != nil {
		return "", false, err
	}
	if len(enc) == 0 {
		return "", enabled, nil
	}
	secret, err := openAES(h.encKey, enc)
	if err != nil {
		return "", enabled, err
	}
	return secret, enabled, nil
}

// ── crypto / token helpers ───────────────────────────────────────────────────

// sealAES encrypts plaintext with AES-256-GCM under key (32 bytes); the nonce is
// prepended to the ciphertext.
func sealAES(key []byte, plaintext string) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return nil, err
	}
	return gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil
}

// openAES reverses sealAES.
func openAES(key, blob []byte) (string, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	if len(blob) < gcm.NonceSize() {
		return "", errors.New("auth: totp ciphertext too short")
	}
	nonce, ct := blob[:gcm.NonceSize()], blob[gcm.NonceSize():]
	plaintext, err := gcm.Open(nil, nonce, ct, nil)
	if err != nil {
		return "", err
	}
	return string(plaintext), nil
}

// newToken16 returns a 32-char hex random token (used for TOTP pending logins).
func newToken16() (string, error) {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		return "", err
	}
	return hex.EncodeToString(b[:]), nil
}
