package handlers

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"golang.org/x/crypto/bcrypt"

	"switchboard-core/auth"
	"switchboard-core/config"
	"switchboard-core/crypto"
	"switchboard-core/models"
	"switchboard-core/store"
)

// Claims represents the JWT payload.
//
// bcryptCost is shared across auth.go, settings.go, admin.go
const bcryptCost = 12

type Claims struct {
	UserID string `json:"user_id"`
	Email  string `json:"email"`
	jwt.RegisteredClaims
}

type AuthHandler struct {
	cfg      *config.Config
	stores   store.Stores
	uekCache *crypto.UEKCache
	provider auth.Provider
}

func NewAuthHandler(cfg *config.Config, s store.Stores, uekCache *crypto.UEKCache, provider auth.Provider) *AuthHandler {
	return &AuthHandler{cfg: cfg, stores: s, uekCache: uekCache, provider: provider}
}

func (h *AuthHandler) Register(c *gin.Context) {
	if !h.provider.SupportsRegistration() {
		c.JSON(http.StatusMethodNotAllowed, gin.H{"error": "registration not supported for this auth mode"})
		return
	}

	result, err := h.provider.Register(c, h.stores)
	if err != nil {
		c.JSON(auth.ErrorToHTTPStatus(err), gin.H{"error": err.Error()})
		return
	}

	if result.VaultHint != "" {
		if err := h.initVault(c.Request.Context(), result.User.ID, result.VaultHint); err != nil {
			log.Printf("⚠ Failed to init vault for user %s: %v", result.User.ID, err)
		}
	}

	if !result.User.IsActive {
		c.JSON(http.StatusCreated, gin.H{
			"message": "Account created but requires admin approval",
			"user_id": result.User.ID,
		})
		return
	}

	tokens, err := h.generateTokens(result.User)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate tokens"})
		return
	}

	c.JSON(http.StatusCreated, tokens)
}

func (h *AuthHandler) Login(c *gin.Context) {
	result, err := h.provider.Authenticate(c, h.stores)
	if err != nil {
		c.JSON(auth.ErrorToHTTPStatus(err), gin.H{"error": err.Error()})
		return
	}

	if result.VaultHint != "" {
		h.unlockVault(c.Request.Context(), result.User, result.VaultHint)
	}

	h.stores.Users.UpdateLastLogin(c.Request.Context(), result.User.ID)

	tokens, err := h.generateTokens(result.User)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate tokens"})
		return
	}

	c.JSON(http.StatusOK, tokens)
}

func (h *AuthHandler) Refresh(c *gin.Context) {
	var req struct {
		RefreshToken string `json:"refresh_token" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	tokenHash := hashToken(req.RefreshToken)
	userID, err := h.stores.Users.GetRefreshToken(c.Request.Context(), tokenHash)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
		return
	}

	// Revoke the used token (rotate)
	h.stores.Users.RevokeRefreshToken(c.Request.Context(), tokenHash)

	user, err := h.stores.Users.GetByID(c.Request.Context(), userID)
	if err != nil || !user.IsActive {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found or inactive"})
		return
	}

	tokens, err := h.generateTokens(user)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate tokens"})
		return
	}

	c.JSON(http.StatusOK, tokens)
}

func (h *AuthHandler) Logout(c *gin.Context) {
	var req struct {
		RefreshToken string `json:"refresh_token"`
	}
	c.ShouldBindJSON(&req)

	if req.RefreshToken != "" {
		tokenHash := hashToken(req.RefreshToken)
		h.stores.Users.RevokeRefreshToken(c.Request.Context(), tokenHash)
	}

	// Evict UEK from session cache
	if userID, exists := c.Get("user_id"); exists {
		h.uekCache.Evict(userID.(string))
	}

	// Clear the sb_token cookie so SSR middleware doesn't trust a stale token
	c.SetCookie("sb_token", "", -1, "/", "", false, false)

	c.JSON(http.StatusOK, gin.H{"message": "logged out"})
}

// ── OIDC Flow ──────────────────────────────

// OIDCLogin initiates the authorization code flow by redirecting to the IdP.
// GET /api/v1/auth/oidc/login
func (h *AuthHandler) OIDCLogin(c *gin.Context) {
	oidcProv, ok := h.provider.(*auth.OIDCProvider)
	if !ok {
		c.JSON(http.StatusBadRequest, gin.H{"error": "OIDC not configured"})
		return
	}

	state := uuid.New().String()
	nonce := uuid.New().String()

	// Determine redirect URI
	redirectURI := h.cfg.OIDCRedirectURL
	if redirectURI == "" {
		// Auto-derive from request
		scheme := "https"
		if c.Request.TLS == nil && c.GetHeader("X-Forwarded-Proto") != "https" {
			scheme = "http"
		}
		redirectURI = scheme + "://" + c.Request.Host + h.cfg.BasePath + "/api/v1/auth/oidc/callback"
	}

	// Store state for callback verification
	if err := h.stores.GlobalConfig.SaveOIDCState(c.Request.Context(), state, nonce, c.Query("redirect")); err != nil {
		log.Printf("[auth/oidc] warn: could not store state: %v", err)
	}

	authURL := oidcProv.AuthorizationURL(state, nonce, redirectURI)
	c.Redirect(http.StatusFound, authURL)
}

// OIDCCallback handles the IdP redirect after successful authentication.
// GET /api/v1/auth/oidc/callback?code=...&state=...
func (h *AuthHandler) OIDCCallback(c *gin.Context) {
	oidcProv, ok := h.provider.(*auth.OIDCProvider)
	if !ok {
		c.JSON(http.StatusBadRequest, gin.H{"error": "OIDC not configured"})
		return
	}

	code := c.Query("code")
	state := c.Query("state")

	// Check for IdP error response (Keycloak returns ?error=...&error_description=...)
	if errCode := c.Query("error"); errCode != "" {
		errDesc := c.Query("error_description")
		log.Printf("[auth/oidc] IdP returned error: %s — %s", errCode, errDesc)
		c.JSON(http.StatusUnauthorized, gin.H{"error": "IdP error: " + errCode, "description": errDesc})
		return
	}

	if code == "" || state == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "missing code or state"})
		return
	}

	// Verify state (nonce + redirectTo retrieved but not yet validated — TODO)
	_, _, err := h.stores.GlobalConfig.ConsumeOIDCState(c.Request.Context(), state)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid or expired state"})
		return
	}

	// Clean up stale states (> 10 minutes old)
	_ = h.stores.GlobalConfig.CleanupOIDCState(c.Request.Context())

	// Determine redirect URI (must match what was sent in the login request)
	redirectURI := h.cfg.OIDCRedirectURL
	if redirectURI == "" {
		scheme := "https"
		if c.Request.TLS == nil && c.GetHeader("X-Forwarded-Proto") != "https" {
			scheme = "http"
		}
		redirectURI = scheme + "://" + c.Request.Host + h.cfg.BasePath + "/api/v1/auth/oidc/callback"
	}

	// Exchange code for tokens
	tokenResp, err := oidcProv.ExchangeCode(c.Request.Context(), code, redirectURI)
	if err != nil {
		log.Printf("[auth/oidc] code exchange failed: %v", err)
		c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication failed"})
		return
	}

	// Use the ID token (or access token) to authenticate via the provider
	// Temporarily set the Authorization header so Authenticate() can read it
	tokenToValidate := tokenResp.IDToken
	if tokenToValidate == "" {
		tokenToValidate = tokenResp.AccessToken
	}
	c.Request.Header.Set("Authorization", "Bearer "+tokenToValidate)

	result, err := oidcProv.Authenticate(c, h.stores)
	if err != nil {
		log.Printf("[auth/oidc] authenticate after code exchange failed: %v", err)
		c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication failed"})
		return
	}

	// Issue internal JWT
	tokens, err := h.generateTokens(result.User)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate tokens"})
		return
	}

	h.stores.Users.UpdateLastLogin(c.Request.Context(), result.User.ID)

	// For browser flow: encode tokens into a fragment that the login page JS
	// can pick up, store in localStorage, and redirect to the app.
	// This avoids httpOnly cookie issues — JS needs the token in localStorage.
	accessToken, _ := tokens["access_token"].(string)
	refreshToken, _ := tokens["refresh_token"].(string)
	userJSON, _ := json.Marshal(tokens["user"])

	// Set page-auth cookie too (for SSR middleware)
	c.SetCookie("sb_token", accessToken, 900, "/", "", false, false)

	// Base64-encode the token payload for the fragment
	payload := fmt.Sprintf(`{"access_token":"%s","refresh_token":"%s","user":%s}`,
		accessToken, refreshToken, string(userJSON))
	encoded := base64.StdEncoding.EncodeToString([]byte(payload))

	dest := h.cfg.BasePath + "/login#oidc_result=" + encoded
	c.Redirect(http.StatusFound, dest)
}

func (h *AuthHandler) generateTokens(user *models.User) (gin.H, error) {
	// Access token (15 min)
	accessClaims := Claims{
		UserID: user.ID,
		Email:  user.Email,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			ID:        uuid.New().String(),
		},
	}
	accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
	accessString, err := accessToken.SignedString([]byte(h.cfg.JWTSecret))
	if err != nil {
		return nil, err
	}

	// Refresh token (7 days)
	refreshRaw := uuid.New().String()
	refreshHash := hashToken(refreshRaw)
	expiresAt := time.Now().Add(7 * 24 * time.Hour)

	if err := h.stores.Users.CreateRefreshToken(context.Background(), user.ID, refreshHash, expiresAt); err != nil {
		log.Printf("warn: failed to store refresh token: %v", err)
	}

	return gin.H{
		"access_token":  accessString,
		"refresh_token": refreshRaw,
		"token_type":    "Bearer",
		"expires_in":    900,
		"user": gin.H{
			"id":           user.ID,
			"username":     user.Username,
			"email":        user.Email,
			"display_name": user.DisplayName,
			"handle":       user.Handle,
			"auth_source":  user.AuthSource,
		},
	}, nil
}

func hashToken(token string) string {
	h := sha256.Sum256([]byte(token))
	return hex.EncodeToString(h[:])
}

// ── Vault Lifecycle ─────────────────────────

// DestroyVaultDB nullifies a user's vault columns and deletes personal
// provider configs. This is the shared DB-level operation used by:
//   - unlockVault (stale seal recovery)
//   - BootstrapAdmin / SeedUsers (password rotation at startup)
//   - AdminHandler.destroyVault (admin-initiated reset)
//
// Does NOT evict from UEK cache or write audit logs — callers handle that.
// v0.29.0: accepts stores instead of using database.DB directly.
func DestroyVaultDB(ctx context.Context, stores store.Stores, userID string) (deleted int64) {
	if err := stores.Users.ClearVaultKeys(ctx, userID); err != nil {
		log.Printf("⚠ DestroyVaultDB: failed to clear vault columns for user %s: %v", userID, err)
	}

	return 0
}

// ProbeAndRepairVault checks whether a user's vault can be unlocked with
// the given password. If the vault doesn't exist (vault_set=false), this is
// a no-op. If the vault exists but the seal is stale (encrypted_uek was
// wrapped with a different password), the vault and personal providers are
// destroyed so initVault fires cleanly on next login.
//
// When uekCache is non-nil and the vault unlocks successfully, the UEK is
// cached so that BYOK operations work immediately without requiring a fresh
// login. This is important for bootstrapped/seeded users whose browser
// sessions survive server restarts.
//
// Used by BootstrapAdmin and SeedUsers where the password is known at startup.
// v0.29.0: accepts stores instead of using database.DB directly.
// v0.30.2: accepts optional uekCache to pre-warm vault on restart.
func ProbeAndRepairVault(ctx context.Context, stores store.Stores, userID, password string, uekCache ...*crypto.UEKCache) {
	vaultSet, encryptedUEK, salt, nonce, err := stores.Users.GetVaultKeys(ctx, userID)
	if err != nil || !vaultSet {
		return // no vault to probe
	}

	pdk := crypto.DeriveKeyFromPassword(password, salt)
	uek, err := crypto.UnwrapUEK(encryptedUEK, nonce, pdk)
	if err == nil {
		// Vault seal matches current password — cache UEK if cache provided
		if len(uekCache) > 0 && uekCache[0] != nil {
			uekCache[0].Store(userID, uek)
		}
		return
	}

	// Stale seal: password has actually changed since the vault was sealed
	deleted := DestroyVaultDB(ctx, stores, userID)
	if deleted > 0 {
		log.Printf("  🔐 Vault stale-seal repair for user %s: cleared %d personal provider(s)", userID, deleted)
	} else {
		log.Printf("  🔐 Vault stale-seal repair for user %s: vault columns cleared", userID)
	}
}

// initVault generates a UEK, wraps it with the user's password, and stores
// the encrypted UEK + salt + nonce on the user record. Called on registration
// and on first login for pre-migration users.
func (h *AuthHandler) initVault(ctx context.Context, userID, password string) error {
	if h.uekCache == nil {
		return nil // Vault not configured (e.g. tests)
	}

	uek, err := crypto.GenerateUEK()
	if err != nil {
		return err
	}

	salt, err := crypto.GenerateSalt()
	if err != nil {
		return err
	}

	pdk := crypto.DeriveKeyFromPassword(password, salt)
	encryptedUEK, nonce, err := crypto.WrapUEK(uek, pdk)
	if err != nil {
		return err
	}

	if err := h.stores.Users.InitVaultKeys(ctx, userID, encryptedUEK, salt, nonce); err != nil {
		return err
	}

	// Cache the UEK for the session
	h.uekCache.Store(userID, uek)
	log.Printf("  🔐 Vault initialized for user %s", userID)
	return nil
}

// unlockVault unwraps the UEK on login and caches it. If the user doesn't
// have a vault yet (pre-migration account), initializes one.
func (h *AuthHandler) unlockVault(ctx context.Context, user *models.User, password string) {
	if h.uekCache == nil {
		return
	}

	vaultSet, encryptedUEK, salt, nonce, err := h.stores.Users.GetVaultKeys(ctx, user.ID)
	if err != nil {
		log.Printf("⚠ Vault query failed for user %s: %v", user.ID, err)
		return
	}

	if !vaultSet {
		// Pre-migration user: initialize vault on first login
		if err := h.initVault(ctx, user.ID, password); err != nil {
			log.Printf("⚠ Vault init on login failed for user %s: %v", user.ID, err)
		}
		return
	}

	// Derive PDK from password and unwrap UEK
	pdk := crypto.DeriveKeyFromPassword(password, salt)
	uek, err := crypto.UnwrapUEK(encryptedUEK, nonce, pdk)
	if err != nil {
		// Stale seal: encrypted_uek was wrapped with a different password
		deleted := DestroyVaultDB(ctx, h.stores, user.ID)
		if deleted > 0 {
			log.Printf("⚠ Vault stale-seal recovery for user %s: cleared %d personal provider(s)", user.ID, deleted)
		} else {
			log.Printf("⚠ Vault stale-seal recovery for user %s: no personal providers to clear", user.ID)
		}

		if err := h.initVault(ctx, user.ID, password); err != nil {
			log.Printf("⚠ Vault re-init failed for user %s: %v", user.ID, err)
		}
		return
	}

	h.uekCache.Store(user.ID, uek)
}

// BootstrapAdmin creates/updates the admin user from env vars (K8s secret).
// When uekCache is provided, the admin's vault is pre-warmed so BYOK
// operations work immediately without requiring a fresh login.
func BootstrapAdmin(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKCache) {
	if cfg.AdminUsername == "" || cfg.AdminPassword == "" {
		return
	}

	ctx := context.Background()
	hash, err := bcrypt.GenerateFromPassword([]byte(cfg.AdminPassword), bcryptCost)
	if err != nil {
		log.Printf("⚠ Failed to hash admin password: %v", err)
		return
	}

	existing, _ := s.Users.GetByUsername(ctx, cfg.AdminUsername)
	if existing != nil {
		// Update password
		s.Users.Update(ctx, existing.ID, map[string]interface{}{
			"password_hash": string(hash),
			"is_active":     true,
		})
		// If the actual password changed, the vault seal is stale. Probe it
		// with the current password and destroy only if unwrap fails.
		// Ensure handle exists (backfill for pre-v0.24.0 users)
		if existing.Handle == "" {
			handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(cfg.AdminUsername))
			s.Users.Update(ctx, existing.ID, map[string]interface{}{"handle": handle})
		}
		var cache *crypto.UEKCache
		if len(uekCache) > 0 {
			cache = uekCache[0]
		}
		ProbeAndRepairVault(ctx, s, existing.ID, cfg.AdminPassword, cache)
		auth.EnsureEveryoneGroup(ctx, s, existing.ID)
		auth.AddToAdminsGroup(ctx, s, existing.ID)
		log.Printf("  ✅ Admin user '%s' updated", cfg.AdminUsername)
		return
	}

	email := cfg.AdminEmail
	if email == "" {
		email = cfg.AdminUsername + "@switchboard.local"
	}

	handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(cfg.AdminUsername))
	user := &models.User{
		Username:     strings.ToLower(cfg.AdminUsername),
		Email:        strings.ToLower(email),
		PasswordHash: string(hash),
		IsActive:     true,
		AuthSource:   "builtin",
		Handle:       handle,
	}

	if err := s.Users.Create(ctx, user); err != nil {
		log.Printf("⚠ Failed to create admin user: %v", err)
		return
	}
	auth.EnsureEveryoneGroup(ctx, s, user.ID)
	auth.AddToAdminsGroup(ctx, s, user.ID)
	log.Printf("  ✅ Admin user '%s' created", cfg.AdminUsername)
}

// SeedUsers creates or updates users from the SEED_USERS env var.
// Format: "user:pass[:admin],user2:pass2" — third field "admin" adds to Admins group.
// Upsert: existing users get their password refreshed on every restart.
// Gated to non-production environments.
func SeedUsers(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKCache) {
	if cfg.SeedUsers == "" {
		log.Printf("  ℹ SEED_USERS not set, skipping")
		return
	}

	if cfg.Environment == "production" {
		log.Printf("⚠ SEED_USERS ignored in production environment")
		return
	}

	ctx := context.Background()
	entries := strings.Split(cfg.SeedUsers, ",")
	log.Printf("  🌱 SEED_USERS: %d entries to process", len(entries))

	for _, entry := range entries {
		entry = strings.TrimSpace(entry)
		if entry == "" {
			continue
		}

		parts := strings.SplitN(entry, ":", 3)
		if len(parts) < 2 {
			log.Printf("⚠ Seed user skipped (bad format, want user:pass[:admin]): %q", entry)
			continue
		}

		username := strings.ToLower(strings.TrimSpace(parts[0]))
		password := strings.TrimSpace(parts[1])
		isAdmin := len(parts) >= 3 && strings.TrimSpace(parts[2]) == "admin"

		if username == "" || password == "" {
			log.Printf("⚠ Seed user skipped (empty username or password)")
			continue
		}

		hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
		if err != nil {
			log.Printf("⚠ Seed user '%s' skipped (hash error): %v", username, err)
			continue
		}

		// Upsert: update password if user exists, create if not
		existing, _ := s.Users.GetByUsername(ctx, username)
		if existing != nil {
			s.Users.Update(ctx, existing.ID, map[string]interface{}{
				"password_hash": string(hash),
				"is_active":     true,
			})
			if existing.Handle == "" {
				handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(username))
				s.Users.Update(ctx, existing.ID, map[string]interface{}{"handle": handle})
			}
			var cache *crypto.UEKCache
			if len(uekCache) > 0 {
				cache = uekCache[0]
			}
			ProbeAndRepairVault(ctx, s, existing.ID, password, cache)
			auth.EnsureEveryoneGroup(ctx, s, existing.ID)
			if isAdmin {
				auth.AddToAdminsGroup(ctx, s, existing.ID)
			}
			log.Printf("  🌱 Seed user '%s' updated (admin=%v)", username, isAdmin)
			continue
		}

		handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(username))
		user := &models.User{
			Username:     username,
			Email:        username + "@switchboard.local",
			PasswordHash: string(hash),
			IsActive:     true,
			AuthSource:   "builtin",
			Handle:       handle,
		}

		if err := s.Users.Create(ctx, user); err != nil {
			log.Printf("⚠ Seed user '%s' failed: %v", username, err)
			continue
		}
		auth.EnsureEveryoneGroup(ctx, s, user.ID)
		if isAdmin {
			auth.AddToAdminsGroup(ctx, s, user.ID)
		}
		log.Printf("  🌱 Seed user '%s' created (admin=%v, active=true)", username, isAdmin)
	}
}

// IsRegistrationEnabled checks the platform policy.
func IsRegistrationEnabled(s store.Stores) bool {
	val, _ := s.Policies.GetBool(context.Background(), "allow_registration")
	return val
}
