package web

import (
	"errors"
	"log/slog"
	"net"
	"net/http"
	"net/mail"
	"strings"

	"backend/internal/auth"
	"backend/internal/db/sqlc"
	"backend/templates"

	"github.com/gorilla/csrf"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgtype"
)

// AuthDeps holds the dependencies shared by all auth handlers.
// Secure should be true in all environments except "dev"/"development";
// it gates the cookie Secure attribute (D-12).
// Limiter is the in-memory rate limiter for POST /login (D-16, AUTH-07).
// When nil, rate limiting is skipped (unit tests that don't exercise that path).
type AuthDeps struct {
	Queries              *sqlc.Queries
	Store                *auth.Store
	Secure               bool
	Limiter              *auth.LimiterStore
	DB                   TxBeginner
	OAuth                auth.OAuthConfig
	GoogleTokenExchanger auth.CodeExchanger
	GoogleVerifier       auth.IDTokenVerifier
}

// errInvalidCreds is the intentionally generic error message for login failures
// (D-20). A single constant ensures both unknown-email and wrong-password paths
// produce the EXACT same body, preventing user enumeration (T-2-03).
const errInvalidCreds = "Invalid email or password"

// clientIP extracts the client IP from r.RemoteAddr after chimw.RealIP has
// already rewritten it (D-17). IP is used only for the rate-limit key and is
// NOT persisted. Uses net.SplitHostPort with fallback to raw value.
func clientIP(r *http.Request) string {
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		// RemoteAddr may already be a plain IP (no port) in some test contexts.
		return r.RemoteAddr
	}
	return host
}

// SignupPageHandler renders the GET /signup page with an empty form.
func SignupPageHandler(deps AuthDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.SignupPage(templates.SignupForm{}, templates.SignupErrors{}, csrf.Token(r), providerButtons(deps)).Render(r.Context(), w)
	}
}

// SignupPostHandler handles POST /signup: validate → hash → insert → create session → redirect.
//
// Security invariants (threat model):
//   - Password length is validated BEFORE calling auth.Hash to prevent long-password DoS (T-2-14).
//   - The raw password is never passed to any template (T-2-01).
//   - Email is not logged on validation errors (T-2-18).
//   - Duplicate email is detected via pgconn error code 23505 (T-2-19).
//   - A fresh session token is created on every signup (T-2-04).
//   - Form values are read via r.PostFormValue ONLY (never r.Body) so gorilla/csrf
//     body consumption does not interfere (Pitfall 1, T-2-08c).
func SignupPostHandler(deps AuthDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		// 1. Read form values via r.PostFormValue (Pitfall 1: never read r.Body directly).
		email := strings.TrimSpace(r.PostFormValue("email"))
		password := r.PostFormValue("password")

		var errs templates.SignupErrors

		// 2. Validate email.
		if _, err := mail.ParseAddress(email); err != nil {
			errs.Email = "Enter a valid email address"
		}

		// 3. Validate password length BEFORE calling Hash (DoS guard T-2-14, D-25).
		if len(password) < 12 {
			errs.Password = "Password must be at least 12 characters"
		} else if len(password) > 128 {
			errs.Password = "Password must be at most 128 characters"
		}

		if errs.Email != "" || errs.Password != "" {
			// Re-populate the email field but NOT the password (T-2-01).
			renderSignupError(w, r, templates.SignupForm{Email: email}, errs, providerButtons(deps), http.StatusUnprocessableEntity)
			return
		}

		// 4. Normalize email (lowercase) before insert; citext handles case-insensitive
		// uniqueness at DB level but we store canonical lowercase for consistency (D-01).
		normalized := strings.ToLower(email)

		// 5. Hash password with production cost parameters.
		hash, err := auth.Hash(password, auth.DefaultParams)
		if err != nil {
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		// 6. Insert user row.
		user, err := deps.Queries.InsertUser(ctx, sqlc.InsertUserParams{
			Email:        normalized,
			PasswordHash: pgtype.Text{String: hash, Valid: true},
		})
		if err != nil {
			var pgErr *pgconn.PgError
			if errors.As(err, &pgErr) && pgErr.Code == "23505" {
				if socialOnly, socialErr := deps.Queries.IsSocialOnlyUserByEmail(ctx, normalized); socialErr == nil && socialOnly {
					errs.Email = "An account already exists for this email. Sign in with your provider."
					renderSignupError(w, r, templates.SignupForm{Email: email}, errs, providerButtons(deps), http.StatusUnprocessableEntity)
					return
				}
				// Unique-constraint violation on email (T-2-19).
				// Specific error message is acceptable on signup per CONTEXT.md specifics.
				errs.Email = "That email is already in use."
				renderSignupError(w, r, templates.SignupForm{Email: email}, errs, providerButtons(deps), http.StatusUnprocessableEntity)
				return
			}
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		// 7. Create session (D-10: fresh token on every signup auto-login).
		cookieValue, expiresAt, err := deps.Store.Create(ctx, user.ID)
		if err != nil {
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		// 8. Set session cookie (D-12).
		auth.SetSessionCookie(w, cookieValue, expiresAt, deps.Secure)

		// 9. Redirect to home (D-11, D-21).
		// HTMX form submissions receive HX-Redirect so HTMX handles navigation client-side.
		// Plain (no-JS) form submissions receive 303 See Other (NOT 302 — Pitfall 9).
		if r.Header.Get("HX-Request") == "true" {
			w.Header().Set("HX-Redirect", "/")
			w.WriteHeader(http.StatusOK)
			return
		}
		http.Redirect(w, r, "/", http.StatusSeeOther)
	}
}

// renderSignupError writes a validation-error response.
// For HTMX requests it renders only the form fragment; for plain requests it
// renders the full page (D-19, D-25).
func renderSignupError(w http.ResponseWriter, r *http.Request, form templates.SignupForm, errs templates.SignupErrors, providers templates.AuthProviderButtons, status int) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(status)
	if r.Header.Get("HX-Request") == "true" {
		_ = templates.SignupFormFragment(form, errs, csrf.Token(r)).Render(r.Context(), w)
	} else {
		_ = templates.SignupPage(form, errs, csrf.Token(r), providers).Render(r.Context(), w)
	}
}

// LoginPageHandler renders the GET /login page with an empty form.
func LoginPageHandler(deps AuthDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = templates.LoginPage(templates.LoginForm{}, templates.LoginErrors{}, csrf.Token(r), providerButtons(deps)).Render(r.Context(), w)
	}
}

// LoginPostHandler handles POST /login:
//  1. Rate-limit check before any other work — even invalid inputs consume tokens (D-16, D-18; gates DoS T-2-14)
//  2. Validate email + password format (specific errors per D-20/D-25)
//  3. Look up user by email
//  4. Verify argon2id password hash
//  5. Rotate session (D-10 fixation defense)
//  6. Set cookie and redirect (D-21)
//
// Security invariants (threat model):
//   - Rate-limit check (rl.Allow) happens BEFORE deps.Queries.GetUserByEmail
//     to prevent argon2 DoS via rapid requests (T-2-14, T-2-09).
//   - Exact same error string (errInvalidCreds const) for unknown email AND
//     wrong password to prevent user enumeration (D-20, T-2-03).
//   - Password is never logged (T-2-21).
//   - Session rotated on every successful login (T-2-04, D-10).
//   - Form values are read via r.PostFormValue ONLY (never r.Body) so gorilla/csrf
//     body consumption does not interfere (Pitfall 1, T-2-08c).
func LoginPostHandler(deps AuthDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		// 1. Read form values via r.PostFormValue (Pitfall 1: never read r.Body directly).
		email := strings.TrimSpace(r.PostFormValue("email"))
		password := r.PostFormValue("password")

		// 2. Rate-limit check FIRST — even on invalid input, to prevent enumeration
		// via timing differences on malformed payloads (D-16, T-2-14).
		// Key uses lowercased raw email + IP so malformed inputs consume rate tokens.
		// Limiter may be nil in tests that don't exercise rate-limiting.
		ip := clientIP(r)
		key := strings.ToLower(email) + ":" + ip
		if deps.Limiter != nil && !deps.Limiter.Allow(key) {
			var rateLimitErrs templates.LoginErrors
			rateLimitErrs.General = "Too many attempts. Try again in a minute."
			renderLoginError(w, r, templates.LoginForm{Email: email}, rateLimitErrs, providerButtons(deps), http.StatusTooManyRequests)
			return
		}

		var errs templates.LoginErrors

		// 3. Validate email (specific error — D-20: validation errors ARE specific).
		if _, err := mail.ParseAddress(email); err != nil {
			errs.Email = "Enter a valid email address"
		}

		// 4. Validate password length BEFORE calling Verify (DoS guard T-2-14, D-25).
		if len(password) < 12 {
			errs.Password = "Password must be at least 12 characters"
		} else if len(password) > 128 {
			errs.Password = "Password must be at most 128 characters"
		}

		if errs.Email != "" || errs.Password != "" {
			renderLoginError(w, r, templates.LoginForm{Email: email}, errs, providerButtons(deps), http.StatusUnprocessableEntity)
			return
		}

		// 5. Normalize email for DB lookup.
		normalized := strings.ToLower(email)

		// 6. Look up user by email.
		user, err := deps.Queries.GetUserByEmail(ctx, normalized)
		if err != nil {
			if errors.Is(err, pgx.ErrNoRows) {
				// D-20: same generic message as wrong-password to prevent enumeration (T-2-03).
				errs.General = errInvalidCreds
				renderLoginError(w, r, templates.LoginForm{Email: email}, errs, providerButtons(deps), http.StatusUnauthorized)
				return
			}
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		// 7. Verify password (argon2id constant-time compare, T-2-13).
		if !user.PasswordHash.Valid {
			// Social-only accounts have no local password. Keep the same generic
			// credential failure used for unknown email and wrong password.
			errs.General = errInvalidCreds
			renderLoginError(w, r, templates.LoginForm{Email: email}, errs, providerButtons(deps), http.StatusUnauthorized)
			return
		}
		ok, err := auth.Verify(user.PasswordHash.String, password)
		if err != nil || !ok {
			// D-20: uses the same constant as unknown-email case (single source of truth).
			errs.General = errInvalidCreds
			renderLoginError(w, r, templates.LoginForm{Email: email}, errs, providerButtons(deps), http.StatusUnauthorized)
			return
		}

		// 8. Rotate session: delete existing session (if any) and create a fresh one (D-10).
		var oldSessionID string
		if sess, _, isAuthed := auth.Authed(ctx); isAuthed {
			oldSessionID = sess.ID
		}
		cookieValue, expiresAt, err := deps.Store.Rotate(ctx, oldSessionID, user.ID)
		if err != nil {
			http.Error(w, "internal server error", http.StatusInternalServerError)
			return
		}

		// 9. Set session cookie (D-12).
		auth.SetSessionCookie(w, cookieValue, expiresAt, deps.Secure)

		// 10. Redirect to home (D-21).
		// HTMX form submissions receive HX-Redirect so HTMX handles navigation client-side.
		// Plain (no-JS) form submissions receive 303 See Other (NOT 302 — Pitfall 9).
		if r.Header.Get("HX-Request") == "true" {
			w.Header().Set("HX-Redirect", "/")
			w.WriteHeader(http.StatusOK)
			return
		}
		http.Redirect(w, r, "/", http.StatusSeeOther)
	}
}

// renderLoginError writes a login validation-error response.
// For HTMX requests it renders only the form fragment; for plain requests it
// renders the full page (D-19).
func renderLoginError(w http.ResponseWriter, r *http.Request, form templates.LoginForm, errs templates.LoginErrors, providers templates.AuthProviderButtons, status int) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(status)
	if r.Header.Get("HX-Request") == "true" {
		_ = templates.LoginFormFragment(form, errs, csrf.Token(r)).Render(r.Context(), w)
	} else {
		_ = templates.LoginPage(form, errs, csrf.Token(r), providers).Render(r.Context(), w)
	}
}

func providerButtons(deps AuthDeps) templates.AuthProviderButtons {
	providers := templates.EmptyAuthProviderButtons()
	providers.Google.Configured = deps.OAuth.Google.Configured()
	providers.Google.StartURL = "/auth/google/start"
	return providers
}

// LogoutHandler handles POST /logout: deletes the session row and clears the
// cookie, then redirects to /login.
//
// Security invariants:
//   - Only reachable via the RequireAuth-gated protected group (D-23).
//   - Defense-in-depth: if somehow reached unauthenticated, redirects to /login.
//   - Store.Delete hard-deletes the session row (D-06, T-2-07).
//   - ClearSessionCookie sets Max-Age=-1 to expire the browser cookie (D-06).
//   - HTMX requests receive 200 + HX-Redirect; plain requests receive 303 (D-22).
//   - CSRF token validated by gorilla/csrf middleware before this handler runs (AUTH-06).
func LogoutHandler(deps AuthDeps) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Defense-in-depth: RequireAuth already gates this route, but guard here
		// too so the handler never panics on a nil session.
		sess, _, ok := auth.Authed(r.Context())
		if !ok {
			http.Redirect(w, r, "/login", http.StatusSeeOther)
			return
		}

		// Hard-delete the session row from the DB (D-06, T-2-07).
		if err := deps.Store.Delete(r.Context(), sess.ID); err != nil {
			slog.Default().Error("logout: delete session", "session_id", sess.ID, "err", err)
			// Continue and clear the cookie even on delete error — partial
			// invalidation is better than leaving the cookie intact.
		}

		// Expire the browser cookie (D-06: Max-Age=-1).
		auth.ClearSessionCookie(w, deps.Secure)

		// HTMX-aware redirect (D-23).
		if r.Header.Get("HX-Request") == "true" {
			w.Header().Set("HX-Redirect", "/login")
			w.WriteHeader(http.StatusOK)
			return
		}
		http.Redirect(w, r, "/login", http.StatusSeeOther)
	}
}
