package codes

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/redis/go-redis/v9"
	"github.com/wangjia/pangolin/server/internal/apierr"
)

// WebhookHandler handles POST /webhook/store/codes requests from the
// card store (发卡店).  It is mounted outside /v1 and requires no JWT.
//
// Security model:
//   - HMAC-SHA256 signature in X-Pangolin-Signature: sha256=<hex>
//   - Unix timestamp in X-Pangolin-Timestamp (±5 min window)
//   - Unique nonce in X-Pangolin-Nonce to prevent replay attacks
//   - Nonce is stored in Redis with a TTL > 2× the timestamp window
type WebhookHandler struct {
	store             *Store
	rdb               *redis.Client
	secret            []byte
	timestampTolerance time.Duration
	nonceTTL          time.Duration
}

// NewWebhookHandler creates a WebhookHandler.
// secret is the raw HMAC key (not hex-encoded).
func NewWebhookHandler(
	store *Store,
	rdb *redis.Client,
	secret string,
	timestampTolerance time.Duration,
	nonceTTL time.Duration,
) *WebhookHandler {
	return &WebhookHandler{
		store:              store,
		rdb:                rdb,
		secret:             []byte(secret),
		timestampTolerance: timestampTolerance,
		nonceTTL:           nonceTTL,
	}
}

// WebhookPayload is the JSON body sent by the card store.
type WebhookPayload struct {
	// Code is the plaintext activation code generated by the card store.
	Code string `json:"code"`
	// Plan is the plan tier (free|pro|team).
	Plan string `json:"plan"`
	// DurationDays is the number of days this code grants.
	DurationDays int `json:"duration_days"`
	// Note is an optional human-readable remark.
	Note string `json:"note,omitempty"`
}

// redisKeyNonce returns the Redis key for a webhook nonce.
func redisKeyNonce(nonce string) string {
	return "webhook:nonce:" + nonce
}

// ServeHTTP implements http.Handler for POST /webhook/store/codes.
func (h *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Read body (limit to 64 KB to prevent DoS).
	body, err := io.ReadAll(io.LimitReader(r.Body, 64*1024))
	if err != nil {
		apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
		return
	}

	// --- Signature verification ---
	if apiErr := h.verifySignature(r, body); apiErr != nil {
		apierr.WriteJSON(w, http.StatusUnauthorized, apiErr)
		return
	}

	// --- Timestamp window ---
	if apiErr := h.verifyTimestamp(r); apiErr != nil {
		apierr.WriteJSON(w, http.StatusUnauthorized, apiErr)
		return
	}

	// --- Nonce deduplication ---
	nonce := r.Header.Get("X-Pangolin-Nonce")
	if nonce == "" {
		apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
		return
	}
	isDupe, apiErr := h.checkAndStoreNonce(r.Context(), nonce)
	if apiErr != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}
	if isDupe {
		// Idempotent: return 200 to acknowledge without re-inserting.
		w.WriteHeader(http.StatusOK)
		_ = json.NewEncoder(w).Encode(map[string]string{"status": "duplicate_ignored"})
		return
	}

	// --- Parse payload ---
	var payload WebhookPayload
	if err := json.Unmarshal(body, &payload); err != nil {
		apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
		return
	}

	if payload.Code == "" || payload.Plan == "" || payload.DurationDays <= 0 {
		apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
		return
	}

	// --- Canonicalize and hash the code ---
	canonical, cerr := Canonicalize(payload.Code)
	if cerr != nil {
		apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrInvalidCode)
		return
	}
	hash := Hash(canonical)

	// --- Idempotent code insertion ---
	ctx := r.Context()

	planID, err := h.store.GetPlanID(ctx, PlanCode(strings.ToLower(payload.Plan)))
	if err != nil {
		apierr.WriteJSON(w, http.StatusBadRequest, apierr.ErrBadRequest)
		return
	}

	// Create batch (one per webhook call; the store may aggregate externally).
	batchID, err := h.store.CreateBatch(ctx, ChannelStore, "webhook", payload.Note)
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}

	err = h.store.CreateCode(ctx, hash, planID, payload.DurationDays, batchID)
	if err == ErrDuplicate {
		// Same code_hash already exists – idempotent, do not error.
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		_ = json.NewEncoder(w).Encode(map[string]string{"status": "already_exists"})
		return
	}
	if err != nil {
		apierr.WriteJSON(w, http.StatusInternalServerError, apierr.ErrInternal)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(map[string]string{"status": "created"})
}

// verifySignature checks the X-Pangolin-Signature header.
// Expected format: "sha256=<lowercase-hex-hmac>"
// HMAC is computed over the raw request body.
func (h *WebhookHandler) verifySignature(r *http.Request, body []byte) *apierr.Error {
	sig := r.Header.Get("X-Pangolin-Signature")
	if sig == "" {
		return apierr.ErrWebhookSignature
	}
	if !strings.HasPrefix(sig, "sha256=") {
		return apierr.ErrWebhookSignature
	}
	gotHex := strings.TrimPrefix(sig, "sha256=")
	gotBytes, err := hex.DecodeString(gotHex)
	if err != nil {
		return apierr.ErrWebhookSignature
	}

	mac := hmac.New(sha256.New, h.secret)
	mac.Write(body)
	expected := mac.Sum(nil)

	// Constant-time comparison to prevent timing attacks.
	if !hmac.Equal(gotBytes, expected) {
		return apierr.ErrWebhookSignature
	}
	return nil
}

// verifyTimestamp checks the X-Pangolin-Timestamp header.
// The timestamp must be within ± h.timestampTolerance of the current time.
func (h *WebhookHandler) verifyTimestamp(r *http.Request) *apierr.Error {
	tsStr := r.Header.Get("X-Pangolin-Timestamp")
	if tsStr == "" {
		return apierr.ErrWebhookTimestamp
	}
	ts, err := strconv.ParseInt(tsStr, 10, 64)
	if err != nil {
		return apierr.ErrWebhookTimestamp
	}
	t := time.Unix(ts, 0)
	now := time.Now().UTC()
	diff := now.Sub(t)
	if diff < 0 {
		diff = -diff
	}
	if diff > h.timestampTolerance {
		return apierr.ErrWebhookTimestamp
	}
	return nil
}

// checkAndStoreNonce checks whether nonce has been seen before.
// If not seen, it stores it in Redis with the nonce TTL and returns false.
// If already seen, it returns true (duplicate).
// Uses SET NX to make the check-and-store atomic.
// Returns an error if rdb is nil (misconfiguration).
func (h *WebhookHandler) checkAndStoreNonce(ctx context.Context, nonce string) (bool, error) {
	if h.rdb == nil {
		return false, fmt.Errorf("webhook: Redis client is not configured")
	}
	key := redisKeyNonce(nonce)
	// SET key "1" NX EX <ttl seconds>
	set, err := h.rdb.SetNX(ctx, key, "1", h.nonceTTL).Result()
	if err != nil {
		return false, fmt.Errorf("webhook checkNonce: %w", err)
	}
	// SetNX returns true if the key was set (not a duplicate).
	return !set, nil
}
