package httpx

import (
	"context"
	"fmt"
	"net/http"
	"net/netip"
	"runtime/debug"
	"time"

	"bountyboard/internal/ulid"
)

type ctxKey int

const (
	ctxKeyRequestID ctxKey = iota
	ctxKeyClient
)

// RequestID returns the id assigned to this request by the middleware chain.
func RequestID(ctx context.Context) string {
	id, _ := ctx.Value(ctxKeyRequestID).(string)
	return id
}

// Client returns the trusted-proxy-resolved client info for this request.
func Client(ctx context.Context) clientInfo {
	c, _ := ctx.Value(ctxKeyClient).(clientInfo)
	return c
}

// ClientIP is a convenience accessor used by rate limiting and audit logging.
func ClientIP(ctx context.Context) netip.Addr { return Client(ctx).IP }

// statusRecorder captures the response status for logging and metrics while
// staying compatible with http.ResponseController (Unwrap).
type statusRecorder struct {
	http.ResponseWriter
	status int
}

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
	}
	return r.ResponseWriter.Write(b)
}

func (r *statusRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter }

func (s *Server) withMiddleware(next http.Handler) http.Handler {
	return s.recoverPanic(s.assignRequestID(s.resolveClientInfo(s.securityHeaders(s.accessLog(next)))))
}

// securityHeaders applies the §12 hardening headers. CSP allows no inline
// scripts — all JS ships as external modules.
func (s *Server) securityHeaders(next http.Handler) http.Handler {
	// 'unsafe-inline' applies to STYLES only (style attributes used across
	// the UI; without it the browser silently drops them). §12 forbids
	// unsafe-inline for scripts, which stays strict.
	const csp = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " +
		"img-src 'self' data:; connect-src 'self'; font-src 'self'; " +
		"frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		h := w.Header()
		h.Set("Content-Security-Policy", csp)
		h.Set("X-Frame-Options", "DENY")
		h.Set("X-Content-Type-Options", "nosniff")
		h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
		next.ServeHTTP(w, r)
	})
}

// recoverPanic guarantees no panic escapes a request path: it logs the stack
// and returns the standard JSON error envelope.
func (s *Server) recoverPanic(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				if rec == http.ErrAbortHandler { // client went away mid-write
					panic(rec)
				}
				s.log.Error("panic in request handler",
					"err", fmt.Sprint(rec),
					"requestId", RequestID(r.Context()),
					"path", r.URL.Path,
					"stack", string(debug.Stack()))
				s.metrics.Inc("http_panics_total", 1)
				writeError(w, http.StatusInternalServerError, "internal", "internal server error")
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func (s *Server) assignRequestID(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := ulid.New()
		w.Header().Set("X-Request-Id", id)
		next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKeyRequestID, id)))
	})
}

func (s *Server) resolveClientInfo(next http.Handler) http.Handler {
	defaultProto := "http"
	if r := s.cfg.AppBaseURL; len(r) >= 8 && r[:8] == "https://" {
		defaultProto = "https"
	}
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		info := resolveClient(r, s.cfg.TrustedProxyCIDRs, defaultProto)
		next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKeyClient, info)))
	})
}

func (s *Server) accessLog(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		rec := &statusRecorder{ResponseWriter: w}
		next.ServeHTTP(rec, r)
		if rec.status == 0 {
			rec.status = http.StatusOK
		}
		s.metrics.Inc("http_requests_total", 1)
		s.metrics.Inc(fmt.Sprintf("http_responses_%dxx_total", rec.status/100), 1)
		s.log.Info("request",
			"method", r.Method,
			"path", r.URL.Path,
			"status", rec.status,
			"durationMs", time.Since(start).Milliseconds(),
			"clientIp", Client(r.Context()).IP.String(),
			"requestId", RequestID(r.Context()))
	})
}
