package httpx

import (
	"context"
	"errors"
	"net/http"
	"time"

	"bountyboard/internal/domain"
	"bountyboard/internal/store"
)

const (
	sessionCookie = "bb_session"
	csrfCookie    = "bb_csrf"
	csrfHeader    = "X-CSRF-Token"
	// sessions slide forward at most once per hour to avoid a write per
	// request
	refreshInterval = time.Hour
)

const (
	ctxKeyUser ctxKey = 100 + iota
	ctxKeySession
)

// CurrentUser returns the authenticated user, or nil outside requireAuth.
func CurrentUser(ctx context.Context) *domain.User {
	u, _ := ctx.Value(ctxKeyUser).(*domain.User)
	return u
}

// CurrentSession returns the active session, or nil outside requireAuth.
func CurrentSession(ctx context.Context) *domain.Session {
	s, _ := ctx.Value(ctxKeySession).(*domain.Session)
	return s
}

func (s *Server) setAuthCookies(w http.ResponseWriter, sessionToken, csrfToken string) {
	secure := s.cfg.CookieSecure()
	http.SetCookie(w, &http.Cookie{
		Name: sessionCookie, Value: sessionToken, Path: "/",
		MaxAge:   int(domain.SessionTTL.Seconds()),
		HttpOnly: true, Secure: secure, SameSite: http.SameSiteLaxMode,
	})
	// Readable by JS so the frontend can mirror it into X-CSRF-Token.
	http.SetCookie(w, &http.Cookie{
		Name: csrfCookie, Value: csrfToken, Path: "/",
		MaxAge:   int(domain.SessionTTL.Seconds()),
		HttpOnly: false, Secure: secure, SameSite: http.SameSiteLaxMode,
	})
}

func (s *Server) clearAuthCookies(w http.ResponseWriter) {
	secure := s.cfg.CookieSecure()
	http.SetCookie(w, &http.Cookie{
		Name: sessionCookie, Value: "", Path: "/", MaxAge: -1,
		HttpOnly: true, Secure: secure, SameSite: http.SameSiteLaxMode,
	})
	http.SetCookie(w, &http.Cookie{
		Name: csrfCookie, Value: "", Path: "/", MaxAge: -1,
		HttpOnly: false, Secure: secure, SameSite: http.SameSiteLaxMode,
	})
}

// mustChangeAllowed are the only API paths reachable while a forced password
// change is pending.
var mustChangeAllowed = map[string]bool{
	"/api/v1/auth/me":              true,
	"/api/v1/auth/logout":          true,
	"/api/v1/auth/logout-all":      true,
	"/api/v1/auth/change-password": true,
}

// requireAuth resolves the session cookie into a user, enforces the disabled
// flag and forced password changes, slides session expiry, and stores both
// in the request context.
func (s *Server) requireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		c, err := r.Cookie(sessionCookie)
		if err != nil || c.Value == "" {
			writeError(w, http.StatusUnauthorized, "unauthenticated", "authentication required")
			return
		}
		sess, err := s.store.SessionByToken(r.Context(), c.Value)
		if err != nil {
			if errors.Is(err, store.ErrNotFound) {
				s.clearAuthCookies(w)
				writeError(w, http.StatusUnauthorized, "unauthenticated", "session expired")
				return
			}
			s.internalError(w, r, "load session", err)
			return
		}
		user, err := s.store.UserByID(r.Context(), sess.UserID)
		if err != nil {
			s.clearAuthCookies(w)
			writeError(w, http.StatusUnauthorized, "unauthenticated", "account no longer exists")
			return
		}
		if user.Disabled {
			_ = s.store.DeleteSession(r.Context(), sess.Token)
			s.clearAuthCookies(w)
			writeError(w, http.StatusForbidden, "account_disabled", "account is disabled")
			return
		}
		if user.MustChangePassword() && !mustChangeAllowed[r.URL.Path] {
			writeError(w, http.StatusForbidden, "password_change_required",
				"password change required before continuing")
			return
		}
		if time.Since(sess.RefreshedAt) > refreshInterval {
			if err := s.store.RefreshSession(r.Context(), sess.Token); err != nil {
				s.log.Warn("refresh session", "err", err)
			}
		}

		ctx := context.WithValue(r.Context(), ctxKeyUser, user)
		ctx = context.WithValue(ctx, ctxKeySession, sess)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// requireCSRF enforces the double-submit check on mutating methods: the
// non-httpOnly cookie value must be echoed in the X-CSRF-Token header.
func (s *Server) requireCSRF(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet, http.MethodHead, http.MethodOptions:
			next.ServeHTTP(w, r)
			return
		}
		c, err := r.Cookie(csrfCookie)
		if err != nil || c.Value == "" || r.Header.Get(csrfHeader) != c.Value {
			writeError(w, http.StatusForbidden, "csrf", "missing or mismatched CSRF token")
			return
		}
		next.ServeHTTP(w, r)
	})
}

// requireRole gates a handler on a role flag; must run inside requireAuth.
func (s *Server) requireRole(role string, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		u := CurrentUser(r.Context())
		if u == nil || !u.Roles.Has(role) {
			writeError(w, http.StatusForbidden, "forbidden", "requires "+role+" role")
			return
		}
		next.ServeHTTP(w, r)
	})
}

// authed wires the standard chain for an authenticated JSON endpoint.
func (s *Server) authed(h http.HandlerFunc) http.Handler {
	return s.requireAuth(s.requireCSRF(h))
}

// authedRole wires authentication + CSRF + a role gate.
func (s *Server) authedRole(role string, h http.HandlerFunc) http.Handler {
	return s.requireAuth(s.requireCSRF(s.requireRole(role, h)))
}

func (s *Server) internalError(w http.ResponseWriter, r *http.Request, what string, err error) {
	s.log.Error(what, "err", err, "requestId", RequestID(r.Context()), "path", r.URL.Path)
	writeError(w, http.StatusInternalServerError, "internal", "internal server error")
}
