package auth

import (
	"context"
	"log/slog"
	"net/http"
)

// sessionCtxKey is the unexported context key type for session data owned by
// this package. Using an unexported named struct prevents collisions with
// other packages' context keys.
type sessionCtxKey struct{}

// sessionKey is the singleton key value used in context.WithValue.
var sessionKey = sessionCtxKey{}

// authed holds the resolved session and user attached to a request context.
type authed struct {
	Session *Session
	User    *User
}

// Authed extracts the session and user from the request context.
// Returns (session, user, true) when a valid session is present, and
// (nil, nil, false) when the request is unauthenticated.
func Authed(ctx context.Context) (*Session, *User, bool) {
	a, ok := ctx.Value(sessionKey).(*authed)
	if !ok || a == nil {
		return nil, nil, false
	}
	return a.Session, a.User, true
}

// ResolveSession reads the session cookie, looks up the session + user, and
// attaches them to the request context. It NEVER blocks the request — missing
// or invalid sessions are silently ignored; RequireAuth enforces access.
//
// When store is nil (e.g. in Phase 1 unit tests that pass a zero AuthDeps),
// the middleware is a no-op pass-through. Cookie resolution only happens when
// store is non-nil and a cookie is present.
//
// On a valid session hit, MaybeExtend is called best-effort (logged but not
// fatal) to implement the sliding 30-day TTL (D-09).
func ResolveSession(store *Store) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Nil-store guard: Phase 1 route tests pass AuthDeps{} (zero value).
			if store == nil {
				next.ServeHTTP(w, r)
				return
			}

			cookie, err := r.Cookie(SessionCookieName)
			if err != nil || cookie.Value == "" {
				// No cookie — pass through unauthenticated.
				next.ServeHTTP(w, r)
				return
			}

			sess, user, err := store.Lookup(r.Context(), cookie.Value)
			if err != nil {
				// Invalid / expired / tampered cookie — do NOT clear the cookie
				// here; the handler or RequireAuth will decide what to do.
				next.ServeHTTP(w, r)
				return
			}

			// Session found — attempt lazy extension (D-09). Best-effort: log on
			// error but do not fail the request.
			if extErr := store.MaybeExtend(r.Context(), sess.ID, sess.ExpiresAt); extErr != nil {
				slog.Default().Warn("session extend failed", "session_id", sess.ID, "err", extErr)
			}

			// Attach session + user to context for downstream handlers.
			ctx := context.WithValue(r.Context(), sessionKey, &authed{Session: sess, User: user})
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

// RequireAuth is middleware that enforces an authenticated session.
// If no session is present in the context (set by ResolveSession), it
// redirects unauth requests to /login:
//   - HTMX requests (HX-Request: true) → 200 with HX-Redirect: /login header
//   - Plain requests → 303 See Other with Location: /login
//
// 303 is mandated (not 302) per D-23 and Pitfall 9: POST/Redirect/GET pattern
// requires 303 to guarantee the redirect uses GET.
func RequireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if _, _, ok := Authed(r.Context()); !ok {
			redirectTo(w, r, "/login")
			return
		}
		next.ServeHTTP(w, r)
	})
}

// RedirectIfAuthed bounces already-authenticated users away from auth pages
// (e.g. /login, /signup) to the home route. This prevents authed users from
// accidentally re-logging-in and rotating their session unnecessarily.
//   - HTMX requests → 200 with HX-Redirect: /
//   - Plain requests → 303 See Other with Location: /
func RedirectIfAuthed(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if _, _, ok := Authed(r.Context()); ok {
			redirectTo(w, r, "/")
			return
		}
		next.ServeHTTP(w, r)
	})
}

// redirectTo performs an HTMX-aware redirect:
//   - When the request carries HX-Request: true, it returns 200 with an
//     HX-Redirect header so HTMX can handle the navigation client-side
//     (Pattern 5 — avoids confusing HTMX with a 303 response).
//   - For plain browser requests it uses 303 See Other (NOT 302 — Pitfall 9).
func redirectTo(w http.ResponseWriter, r *http.Request, target string) {
	if r.Header.Get("HX-Request") == "true" {
		w.Header().Set("HX-Redirect", target)
		w.WriteHeader(http.StatusOK)
		return
	}
	http.Redirect(w, r, target, http.StatusSeeOther)
}
