package httpserver

import (
	"encoding/json"
	"html/template"
	"net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"

	"github.com/tim/cairnquire/apps/server/internal/auth"
)

type passwordRegisterRequest struct {
	Email       string `json:"email"`
	DisplayName string `json:"displayName"`
	Password    string `json:"password"`
	Role        string `json:"role"`
}

type passwordLoginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

type initialSetupRequest struct {
	Email          string `json:"email"`
	DisplayName    string `json:"displayName"`
	Password       string `json:"password"`
	SignupsEnabled bool   `json:"signupsEnabled"`
}

type passkeyBeginRequest struct {
	Email       string `json:"email"`
	DisplayName string `json:"displayName"`
	Role        string `json:"role"`
}

type passkeyFinishRequest struct {
	ChallengeID string `json:"challengeId"`
}

type createTokenRequest struct {
	Name      string       `json:"name"`
	Scopes    []auth.Scope `json:"scopes"`
	ExpiresAt *time.Time   `json:"expiresAt"`
}

type deviceStartRequest struct {
	Name   string       `json:"name"`
	Scopes []auth.Scope `json:"scopes"`
}

type changePasswordRequest struct {
	CurrentPassword string `json:"currentPassword"`
	NewPassword     string `json:"newPassword"`
}

type passwordResetRequest struct {
	Token       string `json:"token"`
	NewPassword string `json:"newPassword"`
}

type deleteAccountRequest struct {
	CurrentPassword string `json:"currentPassword"`
}

type updateUserRoleRequest struct {
	Role string `json:"role"`
}

func (s *Server) handleLoginPage(w http.ResponseWriter, r *http.Request) {
	if _, ok := requirePrincipal(r); ok {
		http.Redirect(w, r, "/account", http.StatusSeeOther)
		return
	}
	nextPath := r.URL.Query().Get("next")
	if !strings.HasPrefix(nextPath, "/") || strings.HasPrefix(nextPath, "//") {
		nextPath = "/account"
	}
	settings, err := s.auth.GetInstanceSettings(r.Context())
	if err != nil {
		s.renderError(w, r, http.StatusInternalServerError, "load registration settings")
		return
	}
	s.renderTemplate(w, r, http.StatusOK, "login.gohtml", layoutData{
		Title:        "Sign in",
		BodyClass:    "page-auth",
		BodyTemplate: "login_content",
		Data: struct {
			Next           string
			SignupsEnabled bool
			DevMode        bool
		}{Next: nextPath, SignupsEnabled: settings.SignupsEnabled, DevMode: s.config.DevMode},
	})
}

func (s *Server) handleSetupPage(w http.ResponseWriter, r *http.Request) {
	settings, err := s.auth.GetInstanceSettings(r.Context())
	if err != nil {
		s.renderError(w, r, http.StatusInternalServerError, "load initial setup state")
		return
	}
	if settings.SetupComplete {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}
	s.renderTemplate(w, r, http.StatusOK, "setup.gohtml", layoutData{
		Title:        "Initial setup",
		BodyClass:    "page-auth",
		BodyTemplate: "setup_content",
	})
}

func (s *Server) handlePasswordResetPage(w http.ResponseWriter, r *http.Request) {
	s.renderTemplate(w, r, http.StatusOK, "password_reset.gohtml", layoutData{
		Title:        "Reset password",
		BodyClass:    "page-auth",
		BodyTemplate: "password_reset_content",
		Data: struct {
			Token string
		}{Token: strings.TrimSpace(r.URL.Query().Get("token"))},
	})
}

func (s *Server) handleSetup(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	var req initialSetupRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	if _, err := s.auth.CompleteInitialSetup(r.Context(), req.Email, req.DisplayName, req.Password, req.SignupsEnabled); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	principal, token, err := s.auth.LoginPassword(r.Context(), req.Email, req.Password, requestIP(r), r.UserAgent())
	if err != nil {
		writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": "initial admin created; sign in to continue"})
		return
	}
	setSessionCookie(w, r, token, principal.ExpiresAt)
	writeJSONWithStatus(w, http.StatusCreated, map[string]any{"principal": principal})
}

func (s *Server) handleAccountPage(w http.ResponseWriter, r *http.Request) {
	principal, ok := requirePrincipal(r)
	if !ok {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}
	s.renderTemplate(w, r, http.StatusOK, "account.gohtml", layoutData{
		Title:        "Account",
		BodyClass:    "page-account",
		BodyTemplate: "account_content",
		Data:         principal,
	})
}

func (s *Server) handleTokenCreatePage(w http.ResponseWriter, r *http.Request) {
	principal, ok := requirePrincipal(r)
	if !ok {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}
	s.renderTemplate(w, r, http.StatusOK, "token_create.gohtml", layoutData{
		Title:        "Create access token",
		BodyClass:    "page-account",
		BodyTemplate: "token_create_content",
		Data:         principal,
	})
}

func (s *Server) handleAuthMe(w http.ResponseWriter, r *http.Request) {
	principal, ok := requirePrincipal(r)
	if !ok {
		writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "authentication required"})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"principal": principal})
}

func (s *Server) handlePasswordRegister(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	var req passwordRegisterRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	user, err := s.auth.RegisterPasswordUser(r.Context(), req.Email, req.DisplayName, req.Password, req.Role)
	if err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSONWithStatus(w, http.StatusCreated, map[string]any{"user": publicUser(user)})
}

func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	var req passwordLoginRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	principal, token, err := s.auth.LoginPassword(r.Context(), req.Email, req.Password, requestIP(r), r.UserAgent())
	if err != nil {
		writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
		return
	}
	setSessionCookie(w, r, token, principal.ExpiresAt)
	writeJSON(w, http.StatusOK, map[string]any{"principal": principal})
}

func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if principal.SessionID != "" {
		_ = s.auth.RevokeSession(r.Context(), principal.SessionID)
	}
	clearSessionCookie(w)
	if s.config.DevMode {
		setDevLogoutCookie(w, r)
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "logged_out"})
}

func (s *Server) handleDevLogin(w http.ResponseWriter, r *http.Request) {
	if !s.config.DevMode || s.devUserID == "" {
		writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "dev login is not available"})
		return
	}
	principal, err := s.auth.PrincipalForUser(r.Context(), s.devUserID)
	if err != nil {
		writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	clearDevLogoutCookie(w)
	writeJSON(w, http.StatusOK, map[string]any{"principal": principal})
}

func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if !requireSessionPrincipal(w, principal) {
		return
	}
	var req changePasswordRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	if err := s.auth.ChangePassword(r.Context(), principal, req.CurrentPassword, req.NewPassword); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "password_changed"})
}

func (s *Server) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	var req passwordResetRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	if err := s.auth.ResetPassword(r.Context(), req.Token, req.NewPassword); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "password_reset"})
}

func (s *Server) handleDeleteAccount(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if !requireSessionPrincipal(w, principal) {
		return
	}
	var req deleteAccountRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	if err := s.auth.DeleteAccount(r.Context(), principal, req.CurrentPassword); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	clearSessionCookie(w)
	writeJSON(w, http.StatusOK, map[string]string{"status": "account_deleted"})
}

func (s *Server) handlePasskeyRegisterBegin(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	var req passkeyBeginRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	options, challengeID, err := s.auth.BeginPasskeyRegistration(r.Context(), req.Email, req.DisplayName, req.Role)
	if err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"challengeId": challengeID, "options": options})
}

func (s *Server) handlePasskeyRegisterFinish(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	challengeID := r.URL.Query().Get("challengeId")
	if challengeID == "" {
		var req passkeyFinishRequest
		_ = json.NewDecoder(r.Body).Decode(&req)
		challengeID = req.ChallengeID
	}
	if challengeID == "" {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "challengeId is required"})
		return
	}
	user, err := s.auth.FinishPasskeyRegistration(r.Context(), challengeID, r)
	if err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"user": publicUser(user)})
}

func (s *Server) handleCurrentUserPasskeyRegisterBegin(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if !requireSessionPrincipal(w, principal) {
		return
	}
	options, challengeID, err := s.auth.BeginCurrentUserPasskeyRegistration(r.Context(), principal)
	if err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"challengeId": challengeID, "options": options})
}

func (s *Server) handleCurrentUserPasskeyRegisterFinish(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if !requireSessionPrincipal(w, principal) {
		return
	}
	challengeID := r.URL.Query().Get("challengeId")
	if challengeID == "" {
		var req passkeyFinishRequest
		_ = json.NewDecoder(r.Body).Decode(&req)
		challengeID = req.ChallengeID
	}
	if challengeID == "" {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "challengeId is required"})
		return
	}
	user, err := s.auth.FinishCurrentUserPasskeyRegistration(r.Context(), principal, challengeID, r)
	if err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"user": publicUser(user)})
}

func (s *Server) handlePasskeyLoginBegin(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	var req passkeyBeginRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	options, challengeID, err := s.auth.BeginPasskeyLogin(r.Context(), req.Email)
	if err != nil {
		writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"challengeId": challengeID, "options": options})
}

func (s *Server) handlePasskeyLoginFinish(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	challengeID := r.URL.Query().Get("challengeId")
	if challengeID == "" {
		var req passkeyFinishRequest
		_ = json.NewDecoder(r.Body).Decode(&req)
		challengeID = req.ChallengeID
	}
	if challengeID == "" {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "challengeId is required"})
		return
	}
	principal, token, err := s.auth.FinishPasskeyLogin(r.Context(), challengeID, r)
	if err != nil {
		writeJSONWithStatus(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
		return
	}
	setSessionCookie(w, r, token, principal.ExpiresAt)
	writeJSON(w, http.StatusOK, map[string]any{"principal": principal})
}

func (s *Server) handleListAPITokens(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if !requireSessionPrincipal(w, principal) {
		return
	}
	keys, err := s.auth.ListAPIKeys(r.Context(), principal.UserID)
	if err != nil {
		writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"tokens": keys})
}

func (s *Server) handleCreateAPIToken(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if !requireSessionPrincipal(w, principal) {
		return
	}
	var req createTokenRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	created, err := s.auth.CreateAPIKey(r.Context(), principal.UserID, req.Name, req.Scopes, req.ExpiresAt)
	if err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSONWithStatus(w, http.StatusCreated, created)
}

func (s *Server) handleRevokeAPIToken(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if !requireSessionPrincipal(w, principal) {
		return
	}
	keyID := chi.URLParam(r, "id")
	if !strings.HasPrefix(keyID, "api:") {
		keyID = "api:" + keyID
	}
	if err := s.auth.RevokeAPIKey(r.Context(), principal.UserID, keyID); err != nil {
		writeJSONWithStatus(w, http.StatusNotFound, map[string]string{"error": "token not found"})
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"})
}

func (s *Server) handleDeviceStart(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	var req deviceStartRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	start, err := s.auth.StartDeviceFlow(r.Context(), req.Name, req.Scopes)
	if err != nil {
		writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, start)
}

func (s *Server) handleDeviceApprove(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if !requireSessionPrincipal(w, principal) {
		return
	}
	var req struct {
		UserCode string `json:"userCode"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	device, err := s.auth.ApproveDeviceFlow(r.Context(), principal, req.UserCode)
	if err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid or expired code"})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"status": "approved", "device": map[string]any{"name": device.Name, "scopes": device.Scopes}})
}

func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) {
	if !s.allowAuthAttempt(w, r) {
		return
	}
	var req struct {
		DeviceCode string `json:"deviceCode"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	created, code, err := s.auth.PollDeviceFlow(r.Context(), req.DeviceCode)
	if err != nil {
		status := http.StatusBadRequest
		if code == "authorization_pending" {
			status = http.StatusAccepted
		}
		writeJSONWithStatus(w, status, map[string]string{"error": code})
		return
	}
	writeJSON(w, http.StatusOK, created)
}

func (s *Server) handleDeviceVerify(w http.ResponseWriter, r *http.Request) {
	userCode := template.HTMLEscapeString(r.URL.Query().Get("user_code"))
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = w.Write([]byte(`<!doctype html><html><body><main><h1>Approve Device</h1><p>Sign in, then approve this code with <code>POST /api/device/approve</code>.</p><p>User code: <strong>` + userCode + `</strong></p></main></body></html>`))
}

func (s *Server) handleDeviceVerifyPage(w http.ResponseWriter, r *http.Request) {
	userCode := strings.TrimSpace(r.URL.Query().Get("user_code"))
	s.renderTemplate(w, r, http.StatusOK, "device_verify.gohtml", layoutData{
		Title:        "Approve device",
		BodyClass:    "page-auth",
		BodyTemplate: "device_verify_content",
		Data: struct {
			UserCode string
		}{UserCode: userCode},
	})
}

func (s *Server) handleAdminUpdateUserRole(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	userID := chi.URLParam(r, "id")
	var req updateUserRoleRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	user, err := s.auth.UpdateUserRole(r.Context(), principal, userID, req.Role)
	if err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"user": publicUser(user)})
}

func (s *Server) handleAdminAuthUsers(w http.ResponseWriter, r *http.Request) {
	users, err := s.auth.ListUsers(r.Context())
	if err != nil {
		writeJSONWithStatus(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	public := make([]map[string]any, 0, len(users))
	for _, user := range users {
		public = append(public, publicUser(user))
	}
	writeJSON(w, http.StatusOK, map[string]any{"users": public})
}

func (s *Server) handleAdminUpdateAuthUsers(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	var req struct {
		Users []auth.UserAccessUpdate `json:"users"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	users, err := s.auth.UpdateUserAccess(r.Context(), principal, req.Users)
	if err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	public := make([]map[string]any, 0, len(users))
	for _, user := range users {
		public = append(public, publicUser(user))
	}
	writeJSON(w, http.StatusOK, map[string]any{"users": public})
}

func (s *Server) handleAdminSendPasswordReset(w http.ResponseWriter, r *http.Request) {
	principal, _ := requirePrincipal(r)
	if err := s.auth.SendPasswordReset(r.Context(), principal, chi.URLParam(r, "id")); err != nil {
		writeJSONWithStatus(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "password_reset_sent"})
}

func (s *Server) allowAuthAttempt(w http.ResponseWriter, r *http.Request) bool {
	if s.authLimiter.Allow(authRateLimitKey(r)) {
		return true
	}
	writeJSONWithStatus(w, http.StatusTooManyRequests, map[string]string{"error": "rate limited"})
	return false
}

func setSessionCookie(w http.ResponseWriter, r *http.Request, token string, expires time.Time) {
	clearDevLogoutCookie(w)
	http.SetCookie(w, &http.Cookie{
		Name:     auth.SessionCookieName,
		Value:    token,
		Path:     "/",
		Expires:  expires,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
		Secure:   isSecureRequest(r),
	})
}

func setDevLogoutCookie(w http.ResponseWriter, r *http.Request) {
	http.SetCookie(w, &http.Cookie{
		Name:     devLogoutCookieName,
		Value:    "1",
		Path:     "/",
		MaxAge:   int((24 * time.Hour).Seconds()),
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
		Secure:   isSecureRequest(r),
	})
}

func clearDevLogoutCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name:     devLogoutCookieName,
		Value:    "",
		Path:     "/",
		MaxAge:   -1,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
	})
}

func clearSessionCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name:     auth.SessionCookieName,
		Value:    "",
		Path:     "/",
		MaxAge:   -1,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
	})
}

func publicUser(user auth.User) map[string]any {
	return map[string]any{
		"id":          user.ID,
		"email":       user.Email,
		"displayName": user.DisplayName,
		"role":        user.Role,
		"disabled":    user.Disabled,
		"createdAt":   user.CreatedAt,
		"lastSeenAt":  user.LastSeenAt,
	}
}

func requireSessionPrincipal(w http.ResponseWriter, principal auth.Principal) bool {
	if principal.Method == "session" {
		return true
	}
	writeJSONWithStatus(w, http.StatusForbidden, map[string]string{"error": "browser session required"})
	return false
}

func requestIP(r *http.Request) string {
	if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
		return strings.TrimSpace(strings.Split(forwarded, ",")[0])
	}
	return r.RemoteAddr
}

func isSecureRequest(r *http.Request) bool {
	return r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https")
}
