package middleware

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"net/http"
	"time"

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

	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	redislib "github.com/TaruLab/keluargamart-backend/pkg/redis"
)

// Idempotency enforces the §19 contract on critical mutations:
//   - Idempotency-Key is mandatory (422 when missing/invalid).
//   - key scope = authenticated user + normalized endpoint (method + route).
//   - fingerprint = method + route + user + request body.
//   - same key + same payload → replay of the stored response.
//   - same key + different payload → 409 conflict.
//   - concurrent identical requests → execute once; the losers wait and replay.
//   - records expire after TTL.
//
// Unlike the rate limiter (which fails open), idempotency FAILS CLOSED: when
// Redis is required but unavailable the request is rejected with 503 so a
// critical mutation can never run twice.
type Idempotency struct {
	rdb       *redis.Client
	required  bool
	ttl       time.Duration
	stale     time.Duration
	wait      time.Duration
	pollEvery time.Duration
	userID    func(r *http.Request) int64
}

// IdempotencyOption tweaks the middleware.
type IdempotencyOption func(*Idempotency)

// WithRequired marks Redis mandatory: unavailable → 503 (fail closed).
func WithRequired(required bool) IdempotencyOption {
	return func(im *Idempotency) { im.required = required }
}

// WithTTL bounds how long a completed response can be replayed.
func WithTTL(ttl time.Duration) IdempotencyOption {
	return func(im *Idempotency) { im.ttl = ttl }
}

// WithUserID injects the authenticated user id extractor.
func WithUserID(fn func(r *http.Request) int64) IdempotencyOption {
	return func(im *Idempotency) { im.userID = fn }
}

// NewIdempotency builds the middleware. rdb may be nil for local dev; with
// WithRequired(true) nil/errors produce 503 instead of passing through.
func NewIdempotency(rdb *redis.Client, opts ...IdempotencyOption) *Idempotency {
	im := &Idempotency{
		rdb:       rdb,
		required:  false,
		ttl:       24 * time.Hour,
		stale:     30 * time.Second,
		wait:      3 * time.Second,
		pollEvery: 50 * time.Millisecond,
		userID:    func(r *http.Request) int64 { return 0 },
	}
	for _, o := range opts {
		o(im)
	}
	return im
}

// Handler wraps a mutation with the idempotency guard.
func (im *Idempotency) Handler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if im.rdb == nil {
			if im.required {
				httpresponse.Error(w, http.StatusServiceUnavailable, RequestIDFrom(r.Context()),
					httpresponse.CodeInternal, "idempotency store unavailable", nil)
				return
			}
			next.ServeHTTP(w, r)
			return
		}
		if !isMutation(r.Method) {
			next.ServeHTTP(w, r)
			return
		}

		key := r.Header.Get("Idempotency-Key")
		if key == "" {
			if im.required {
				httpresponse.Error(w, http.StatusUnprocessableEntity, RequestIDFrom(r.Context()),
					httpresponse.CodeValidation, "Idempotency-Key header is required", nil)
				return
			}
			next.ServeHTTP(w, r)
			return
		}
		if !validIdemKey(key) {
			httpresponse.Error(w, http.StatusUnprocessableEntity, RequestIDFrom(r.Context()),
				httpresponse.CodeValidation, "Idempotency-Key must be 1-255 characters", nil)
			return
		}

		body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
		if err != nil {
			httpresponse.Error(w, http.StatusInternalServerError, RequestIDFrom(r.Context()),
				httpresponse.CodeInternal, "failed to read request body", nil)
			return
		}
		r.Body = io.NopCloser(bytes.NewReader(body))

		route := routePattern(r)
		userID := im.userID(r)
		fp := fingerprint(r.Method, route, userID, body)
		idemKey := redislib.IdemKey(userID, route, key)

		acquired, rep, err := redislib.Claim(r.Context(), im.rdb, idemKey, fp, im.ttl, im.stale)
		if err != nil && !errors.Is(err, redislib.ErrIdempotencyConflict) {
			if im.required {
				httpresponse.Error(w, http.StatusServiceUnavailable, RequestIDFrom(r.Context()),
					httpresponse.CodeInternal, "idempotency store unavailable", nil)
				return
			}
			next.ServeHTTP(w, r)
			return
		}
		if rep != nil {
			im.replay(w, rep)
			return
		}
		if !acquired {
			if errors.Is(err, redislib.ErrIdempotencyConflict) {
				httpresponse.Error(w, http.StatusConflict, RequestIDFrom(r.Context()),
					httpresponse.CodeConflict, "Idempotency-Key already used with a different request", nil)
				return
			}
			// concurrent in-flight request with the same key+payload: wait, then replay
			deadline := time.Now().Add(im.wait)
			for time.Now().Before(deadline) {
				time.Sleep(im.pollEvery)
				rep, err := redislib.Replay(r.Context(), im.rdb, idemKey, fp)
				if err != nil {
					if errors.Is(err, redislib.ErrIdempotencyConflict) {
						httpresponse.Error(w, http.StatusConflict, RequestIDFrom(r.Context()),
							httpresponse.CodeConflict, "Idempotency-Key already used with a different request", nil)
						return
					}
					// Redis failure while waiting: same contract as Claim above.
					// Fail closed rather than silently polling to a 409 for a
					// mutation that may have executed.
					if im.required {
						httpresponse.Error(w, http.StatusServiceUnavailable, RequestIDFrom(r.Context()),
							httpresponse.CodeInternal, "idempotency store unavailable", nil)
						return
					}
					continue
				}
				if rep != nil {
					im.replay(w, rep)
					return
				}
			}
			httpresponse.Error(w, http.StatusConflict, RequestIDFrom(r.Context()),
				httpresponse.CodeConflict, "concurrent request in progress", nil)
			return
		}

		rec := &statusRecorder{ResponseWriter: w}
		next.ServeHTTP(rec, r)
		// store completed responses for replay; failures stay retryable
		if rec.status < 500 {
			_ = redislib.Complete(r.Context(), im.rdb, idemKey, fp, im.ttl, rec.status, rec.body(), rec.contentType())
		}
	})
}

func (im *Idempotency) replay(w http.ResponseWriter, rep *redislib.IdemResult) {
	if rep.ContentType != "" {
		w.Header().Set("Content-Type", rep.ContentType)
	}
	w.WriteHeader(rep.Status)
	_, _ = w.Write(rep.Body)
}

func isMutation(method string) bool {
	switch method {
	case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
		return true
	}
	return false
}

func validIdemKey(k string) bool {
	if len(k) == 0 || len(k) > 255 {
		return false
	}
	for _, c := range k {
		if c < 0x21 || c > 0x7e {
			return false
		}
	}
	return true
}

func routePattern(r *http.Request) string {
	if rc := chi.RouteContext(r.Context()); rc != nil {
		return rc.RoutePattern()
	}
	return r.URL.Path
}

// fingerprint uniquely identifies a logical request within its scope.
func fingerprint(method, route string, userID int64, body []byte) string {
	h := sha256.New()
	_, _ = fmt.Fprintf(h, "%s|%s|%d|", method, route, userID)
	_, _ = h.Write(body)
	return hex.EncodeToString(h.Sum(nil))
}

// statusRecorder captures the status + body for replay storage.
type statusRecorder struct {
	http.ResponseWriter
	status int
	buf    bytes.Buffer
}

func (r *statusRecorder) WriteHeader(code int) {
	if r.status == 0 {
		r.status = code
	}
	r.ResponseWriter.WriteHeader(code)
}

func (r *statusRecorder) Write(b []byte) (int, error) {
	if r.status == 0 {
		r.status = http.StatusOK
	}
	r.buf.Write(b)
	return r.ResponseWriter.Write(b)
}

func (r *statusRecorder) body() []byte { return r.buf.Bytes() }

func (r *statusRecorder) contentType() string { return r.Header().Get("Content-Type") }
