package web

import (
	"context"
	"errors"
	"net/http"
	"strings"
)

// Identity is the narrow port the web layer uses to resolve a Dex subject to a
// tapir user and to register new ones (ADR-012). *store.Store satisfies it; tests
// can substitute a fake. It is deliberately separate from Store: identity
// resolution runs pre-scope (un-RLS'd map), whereas Store runs user-scoped.
type Identity interface {
	UserBySubject(ctx context.Context, subject string) (userID string, found bool, err error)
	RegisterUser(ctx context.Context, subject, displayName string) (userID string, err error)
}

// errNoCurrentUser indicates a scoped handler ran without a resolved user_id —
// only possible if it was reached outside the registration gate (a wiring bug).
var errNoCurrentUser = errors.New("web: no current user in request context")

// userIDCtxKey types the per-request resolved tapir user_id stored by the
// registration gate. Unexported so only this package can set it.
type userIDCtxKey struct{}

func withUserID(ctx context.Context, id string) context.Context {
	return context.WithValue(ctx, userIDCtxKey{}, id)
}

// CurrentUserID returns the tapir user_id (UUID) the registration gate resolved
// for the request from the authenticated Dex subject. ok is false for requests
// that never passed the gate (e.g. /register, /auth/*). This is the seam handlers
// — and downstream features (per-user YouTube connect, account management) —
// scope every store access by.
func CurrentUserID(r *http.Request) (string, bool) {
	id, ok := r.Context().Value(userIDCtxKey{}).(string)
	return id, ok && id != ""
}

// registrationGate sits inside Auth.Middleware. For a gated request it resolves
// the authenticated subject → tapir user_id once and stashes it for handlers; a
// subject with no tapir user is redirected to /register. Exempt paths pass
// straight through (/register so an unregistered user can reach the form; /auth/*
// and /healthz are already public but listed for safety).
func (a *App) registrationGate(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if isRegistrationExempt(r.URL.Path) {
			h.ServeHTTP(w, r)
			return
		}
		user, ok := a.Auth.CurrentUser(r)
		if !ok {
			// Auth.Middleware should have caught this; redirect defensively.
			http.Redirect(w, r, loginPath, http.StatusFound)
			return
		}
		userID, found, err := a.Identity.UserBySubject(r.Context(), user.Subject)
		if err != nil {
			a.serverError(w, r, "resolve identity", err)
			return
		}
		if !found {
			http.Redirect(w, r, registerPath, http.StatusFound)
			return
		}
		// Stamp the read-side usage signal (Stage-0 gate, ADR-016): the store
		// throttles this to one row per user per day, so a stamp on every gated
		// request is cheap. Best-effort — a stamp failure must never break the
		// request the user actually came for, so it is logged and swallowed.
		if err := a.Store.StampLogin(r.Context(), userID); err != nil {
			a.logger().Warn("stamp login event", "user", userID, "err", err)
		}
		h.ServeHTTP(w, r.WithContext(withUserID(r.Context(), userID)))
	})
}

// handleRegisterForm renders the registration form for an authenticated, not-yet-
// registered subject. An already-registered subject is sent to the app root.
func (a *App) handleRegisterForm(w http.ResponseWriter, r *http.Request) {
	user, ok := a.Auth.CurrentUser(r)
	if !ok {
		http.Redirect(w, r, loginPath, http.StatusFound)
		return
	}
	if _, found, err := a.Identity.UserBySubject(r.Context(), user.Subject); err != nil {
		a.serverError(w, r, "resolve identity", err)
		return
	} else if found {
		http.Redirect(w, r, "/", http.StatusSeeOther)
		return
	}
	a.render(w, r, RegisterPage(user.Email, ""))
}

// handleRegister creates the tapir user for the authenticated subject from the
// submitted display name (terms must be accepted), then redirects to the app
// root. A double-submit by an already-registered subject is idempotent.
func (a *App) handleRegister(w http.ResponseWriter, r *http.Request) {
	user, ok := a.Auth.CurrentUser(r)
	if !ok {
		http.Redirect(w, r, loginPath, http.StatusFound)
		return
	}
	if _, found, err := a.Identity.UserBySubject(r.Context(), user.Subject); err != nil {
		a.serverError(w, r, "resolve identity", err)
		return
	} else if found {
		http.Redirect(w, r, "/", http.StatusSeeOther)
		return
	}

	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad form", http.StatusBadRequest)
		return
	}
	displayName := strings.TrimSpace(r.FormValue("display_name"))
	if displayName == "" {
		a.renderStatus(w, r, http.StatusBadRequest,
			RegisterPage(user.Email, "Enter a display name to continue."))
		return
	}

	if _, err := a.Identity.RegisterUser(r.Context(), user.Subject, displayName); err != nil {
		a.serverError(w, r, "register user", err)
		return
	}
	setFlash(w, flashRegistered)
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

const (
	registerPath = "/register"
	loginPath    = "/auth/login"
)

func isRegistrationExempt(p string) bool {
	return p == registerPath || p == "/healthz" || strings.HasPrefix(p, "/auth/")
}
