// Package handlers provides HTTP request/response handlers for the bookhoard application.
// It includes handlers for authentication, libraries, media items, reading, collections,
// dashboards, devices, analytics, and various system features.
package handlers

import (
	"bookhoard/internal/database"
	"bookhoard/internal/middleware"
	"bookhoard/internal/setupstatus"
	"context"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/labstack/echo/v5"
	"golang.org/x/crypto/bcrypt"
)

const (
	// DefaultSessionDuration is the fallback session duration used when no
	// settings registry is wired (matches the historical 7-day value).
	DefaultSessionDuration = 7 * 24 * time.Hour
)

// SessionDurationSec is retained for backward compatibility; new code uses the
// registry via AuthHandler.sessionDuration().
var SessionDurationSec = int(DefaultSessionDuration.Seconds())

// SetSettings wires the tunable settings registry (optional).
func (h *AuthHandler) SetSettings(s *database.SettingsRegistry) { h.settings = s }

// sessionDuration returns the active session duration from the registry.
func (h *AuthHandler) sessionDuration() time.Duration {
	if h.settings != nil {
		return h.settings.SessionDuration()
	}
	return DefaultSessionDuration
}

// refreshTokenTTL returns the active refresh-token lifetime (shared with the
// session duration), with a compiled-default fallback.
func (h *AuthHandler) refreshTokenTTL() time.Duration {
	if h.settings != nil {
		return h.settings.SessionDuration()
	}
	return DefaultSessionDuration
}

var secure = os.Getenv("COOKIE_SECURE")

type AuthHandler struct {
	db                  *database.Queries
	jwtKey              []byte
	loginAttemptTracker *middleware.LoginAttemptTracker
	settings            *database.SettingsRegistry
}

func NewAuthHandler(db *database.Queries, jwtSecret string, loginAttemptTracker *middleware.LoginAttemptTracker) *AuthHandler {
	return &AuthHandler{
		db:                  db,
		jwtKey:              []byte(jwtSecret),
		loginAttemptTracker: loginAttemptTracker,
	}
}

type RegisterRequest struct {
	Email     string `form:"email" json:"email" validate:"required,email"`
	Username  string `form:"username" json:"username" validate:"required,min=3,max=50"`
	Password  string `form:"password" json:"password" validate:"required,passwordcomplex"`
	FirstName string `form:"first_name" json:"first_name,omitempty"`
	LastName  string `form:"last_name" json:"last_name,omitempty"`
	Role      string `form:"role" json:"role,omitempty"`
}

type LoginRequest struct {
	Login    string `form:"login" json:"login" validate:"required"` // email or username
	Password string `form:"password" json:"password" validate:"required"`
}

type AuthResponse struct {
	Token        string      `json:"access_token"`
	RefreshToken string      `json:"refresh_token,omitempty"`
	TokenType    string      `json:"token_type"`
	ExpiresIn    int         `json:"expires_in"`
	User         UserProfile `json:"user"`
}

type UserProfile struct {
	ID        string `json:"id"`
	Email     string `json:"email"`
	Username  string `json:"username"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	Role      string `json:"role"`
}

type UpdateProfileRequest struct {
	Username  string `json:"username,omitempty" form:"username" validate:"omitempty,min=3,max=50"`
	Email     string `json:"email,omitempty" form:"email" validate:"omitempty,email"`
	FirstName string `json:"first_name,omitempty" form:"first_name" validate:"omitempty,max=100"`
	LastName  string `json:"last_name,omitempty" form:"last_name" validate:"omitempty,max=100"`
	Theme     string `json:"theme,omitempty" form:"theme" validate:"omitempty"`
	Timezone  string `json:"timezone,omitempty" form:"timezone" validate:"omitempty"`
}

type AdminUpdateUserRequest struct {
	Username  string `json:"username,omitempty" form:"username" validate:"omitempty,min=3,max=50"`
	Email     string `json:"email,omitempty" form:"email" validate:"omitempty,email"`
	FirstName string `json:"first_name,omitempty" form:"first_name" validate:"omitempty,max=100"`
	LastName  string `json:"last_name,omitempty" form:"last_name" validate:"omitempty,max=100"`
	Theme     string `json:"theme,omitempty" form:"theme" validate:"omitempty"`
	Timezone  string `json:"timezone,omitempty" form:"timezone" validate:"omitempty"`
	Role      string `json:"role,omitempty" form:"role" validate:"omitempty,oneof=user admin"`
}

// Register handles POST /api/auth/register
func (h *AuthHandler) Register(c *echo.Context) error {
	email := c.FormValue("email")
	username := c.FormValue("username")
	password := c.FormValue("password")
	firstName := c.FormValue("first_name")
	lastName := c.FormValue("last_name")
	role := c.FormValue("role")

	if email == "" || username == "" || password == "" {
		req := RegisterRequest{}
		if err := c.Bind(&req); err != nil {
			if c.Request().Header.Get("HX-Request") == "true" {
				return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Invalid request</div>`)
			}
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
		}
		if err := c.Validate(&req); err != nil {
			if c.Request().Header.Get("HX-Request") == "true" {
				return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
			}
			return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
		}
		email = req.Email
		username = req.Username
		password = req.Password
		firstName = req.FirstName
		lastName = req.LastName
		role = req.Role
	}

	req := RegisterRequest{Email: email, Username: username, Password: password, FirstName: firstName, LastName: lastName, Role: role}
	if err := c.Validate(&req); err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
		}
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	req.Username = strings.TrimSpace(req.Username)
	if req.Username == "" {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Username cannot be empty or whitespace</div>`)
		}
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "username cannot be empty or whitespace"})
	}

	if req.Role != "" {
		req.Role = strings.ToLower(req.Role)
	}

	if _, err := h.db.GetUserByEmail(c.Request().Context(), req.Email); err == nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusConflict, `<div class="text-red-500">Email already exists</div>`)
		}
		return c.JSON(http.StatusConflict, map[string]string{"error": "email already exists"})
	}

	if _, err := h.db.GetUserByUsername(c.Request().Context(), req.Username); err == nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusConflict, `<div class="text-red-500">Username already exists</div>`)
		}
		return c.JSON(http.StatusConflict, map[string]string{"error": "username already exists"})
	}

	users, err := h.db.ListUsers(c.Request().Context())
	if err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to check existing users: `+err.Error()+`</div>`)
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check existing users: " + err.Error()})
	}

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to hash password</div>`)
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
	}

	adminExists := false
	for _, u := range users {
		if u.Role == "admin" {
			adminExists = true
			break
		}
	}

	var userRole string
	if !adminExists {
		userRole = "admin"
	} else {
		userRole = req.Role
		if userRole == "" {
			userRole = "user"
		}

		if userRole != "user" && userRole != "admin" {
			if c.Request().Header.Get("HX-Request") == "true" {
				return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Invalid role. Must be 'user' or 'admin'</div>`)
			}
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid role. must be 'user' or 'admin'"})
		}

		if userRole == "admin" && adminExists {
			user, ok := c.Get("user").(database.Users)
			if !ok || user.Role != "admin" {
				if c.Request().Header.Get("HX-Request") == "true" {
					return c.HTML(http.StatusForbidden, `<div class="text-red-500">Only existing administrators can create admin accounts</div>`)
				}
				return c.JSON(http.StatusForbidden, map[string]string{"error": "only administrators can create admin accounts"})
			}
		}
	}

	user, err := h.db.CreateUser(c.Request().Context(), database.CreateUserParams{
		Email:        req.Email,
		Username:     req.Username,
		PasswordHash: string(hashedPassword),
		FirstName:    pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""},
		LastName:     pgtype.Text{String: req.LastName, Valid: req.LastName != ""},
		Theme:        pgtype.Text{String: "tokyo-night", Valid: true},
		Role:         userRole,
	})
	if err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">`+err.Error()+`</div>`)
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	// A new user may have changed the admin count (e.g. first user becomes
	// admin), so refresh the setup-status cache.
	setupstatus.Invalidate()

	if err := h.CreateDefaultCollectionsForUser(c.Request().Context(), user.ID); err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to create default collections</div>`)
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create default collections"})
	}

	accessToken, err := h.generateJWTWithAllClaims(
		uuid.UUID(user.ID.Bytes).String(),
		user.Role,
		user.Email,
		user.Username,
	)
	if err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to generate token</div>`)
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
	}

	// Set HTTP-only cookie for browser-based authentication
	cookie := &http.Cookie{
		Name:     "token",
		Value:    accessToken,
		Path:     "/",
		HttpOnly: true,
		Secure:   secure == "true", // TODO: Set to true in production with HTTPS
		SameSite: http.SameSiteLaxMode,
		MaxAge:   int(h.sessionDuration().Seconds()),
	}
	c.SetCookie(cookie)

	_, refreshToken, err := h.CreateRefreshToken(user.ID.Bytes)
	if err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to generate refresh token</div>`)
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate refresh token"})
	}

	if c.Request().Header.Get("HX-Request") == "true" {
		html := fmt.Sprintf(`<div class="text-green-500">Registration successful! Redirecting...</div>
<script>
localStorage.setItem('token', '%s');
localStorage.setItem('refreshToken', '%s');
localStorage.setItem('user', JSON.stringify(%s));
window.location.href = '/dashboard';
</script>`, accessToken, refreshToken, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username))
		return c.HTML(http.StatusCreated, html)
	}

	if user.FirstName.Valid {
		firstName = user.FirstName.String
	}
	if user.LastName.Valid {
		lastName = user.LastName.String
	}
	return c.JSON(http.StatusCreated, AuthResponse{
		Token:        accessToken,
		RefreshToken: refreshToken,
		TokenType:    "Bearer",
		ExpiresIn:    int(h.sessionDuration().Seconds()),
		User: UserProfile{
			ID:        uuid.UUID(user.ID.Bytes).String(),
			Email:     user.Email,
			Username:  user.Username,
			FirstName: firstName,
			LastName:  lastName,
			Role:      user.Role,
		},
	})
}

// Login handles POST /api/auth/login
func (h *AuthHandler) Login(c *echo.Context) error {
	login := c.FormValue("login")
	password := c.FormValue("password")

	if login == "" || password == "" {
		req := LoginRequest{}
		if err := c.Bind(&req); err != nil {
			if c.Request().Header.Get("HX-Request") == "true" {
				return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Invalid request</div>`)
			}
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
		}
		if err := c.Validate(&req); err != nil {
			if c.Request().Header.Get("HX-Request") == "true" {
				return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
			}
			return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
		}
		login = req.Login
		password = req.Password
	}

	req := LoginRequest{Login: login, Password: password}
	if err := c.Validate(&req); err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusBadRequest, `<div class="text-red-500">`+err.Error()+`</div>`)
		}
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	ip := c.RealIP()
	if ip == "" {
		ip = c.Request().RemoteAddr
	}

	locked, remainingTime := h.loginAttemptTracker.IsLocked(login)
	if locked {
		errMsg := fmt.Sprintf("Account locked. Try again in %d minutes", int(remainingTime.Minutes())+1)
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusTooManyRequests, `<div class="text-red-500">`+errMsg+`</div>`)
		}
		return c.JSON(http.StatusTooManyRequests, map[string]string{"error": errMsg})
	}

	user, err := h.db.GetUserForLogin(c.Request().Context(), req.Login)
	if err != nil {
		locked, remainingTime := h.loginAttemptTracker.RecordFailedAttempt(login)
		if locked {
			errMsg := fmt.Sprintf("Too many failed attempts. Account locked for %d minutes", int(remainingTime.Minutes())+1)
			if c.Request().Header.Get("HX-Request") == "true" {
				return c.HTML(http.StatusTooManyRequests, `<div class="text-red-500">`+errMsg+`</div>`)
			}
			return c.JSON(http.StatusTooManyRequests, map[string]string{"error": errMsg})
		}

		if c.Request().Header.Get("HX-Request") == "true" {
			return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
		}
		return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
	}

	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
		locked, remainingTime := h.loginAttemptTracker.RecordFailedAttempt(login)
		if locked {
			errMsg := fmt.Sprintf("Too many failed attempts. Account locked for %d minutes", int(remainingTime.Minutes())+1)
			if c.Request().Header.Get("HX-Request") == "true" {
				return c.HTML(http.StatusTooManyRequests, `<div class="text-red-500">`+errMsg+`</div>`)
			}
			return c.JSON(http.StatusTooManyRequests, map[string]string{"error": errMsg})
		}

		if c.Request().Header.Get("HX-Request") == "true" {
			return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
		}
		return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
	}

	h.loginAttemptTracker.ClearAttempts(login)

	accessToken, err := h.generateJWTWithAllClaims(
		uuid.UUID(user.ID.Bytes).String(),
		user.Role,
		user.Email,
		user.Username,
	)
	if err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to generate token</div>`)
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
	}

	// Set HTTP-only cookie for browser-based authentication
	cookie := &http.Cookie{
		Name:     "token",
		Value:    accessToken,
		Path:     "/",
		HttpOnly: true,
		Secure:   secure == "true", // TODO: Set to true in production with HTTPS
		SameSite: http.SameSiteLaxMode,
		MaxAge:   int(h.sessionDuration().Seconds()),
	}
	c.SetCookie(cookie)

	_, refreshToken, err := h.CreateRefreshToken(user.ID.Bytes)
	if err != nil {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to generate refresh token</div>`)
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate refresh token"})
	}

	if c.Request().Header.Get("HX-Request") == "true" {
		redirect := c.FormValue("redirect")
		if redirect == "" {
			redirect = "/dashboard"
		}
		html := fmt.Sprintf(`<div class="text-green-500">Login successful! Redirecting...</div>
<script>
localStorage.setItem('token', '%s');
localStorage.setItem('refreshToken', '%s');
localStorage.setItem('user', JSON.stringify(%s));
window.location.href = '%s';
</script>`, accessToken, refreshToken, fmt.Sprintf(`{"id":"%s","email":"%s","username":"%s","first_name":"%s","last_name":"%s"}`, uuid.UUID(user.ID.Bytes).String(), user.Email, user.Username, user.FirstName.String, user.LastName.String), redirect)
		return c.HTML(http.StatusOK, html)
	}

	firstName := ""
	if user.FirstName.Valid {
		firstName = user.FirstName.String
	}
	lastName := ""
	if user.LastName.Valid {
		lastName = user.LastName.String
	}
	return c.JSON(http.StatusOK, AuthResponse{
		Token:        accessToken,
		RefreshToken: refreshToken,
		TokenType:    "Bearer",
		ExpiresIn:    int(h.sessionDuration().Seconds()),
		User: UserProfile{
			ID:        uuid.UUID(user.ID.Bytes).String(),
			Email:     user.Email,
			Username:  user.Username,
			FirstName: firstName,
			LastName:  lastName,
			Role:      user.Role,
		},
	})
}

// GetProfile handles GET /api/auth/profile
func (h *AuthHandler) GetProfile(c *echo.Context) error {
	user := MustGetAuthenticatedUser(c)

	firstName := ""
	if user.FirstName.Valid {
		firstName = user.FirstName.String
	}
	lastName := ""
	if user.LastName.Valid {
		lastName = user.LastName.String
	}
	return c.JSON(http.StatusOK, UserProfile{
		ID:        uuid.UUID(user.ID.Bytes).String(),
		Email:     user.Email,
		Username:  user.Username,
		FirstName: firstName,
		LastName:  lastName,
		Role:      user.Role,
	})
}

// UpdateProfile handles PUT /api/auth/profile (self-edit) and PUT /api/auth/profile/:id (admin edit)
// Combined handler for both self-service and admin profile updates
func (h *AuthHandler) UpdateProfile(c *echo.Context) error {
	currentUser := MustGetAuthenticatedUser(c)

	// Determine target user: URL param (admin mode) or current user (self-edit)
	targetUserID := c.Param("id")
	var targetUserUUID pgtype.UUID
	isAdminMode := targetUserID != ""

	if isAdminMode {
		// Admin editing another user - validate admin role
		if currentUser.Role != "admin" {
			return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
		}
		parsedUUID, err := uuid.Parse(targetUserID)
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
		}
		targetUserUUID = pgtype.UUID{Bytes: parsedUUID, Valid: true}
	} else {
		// Self-edit mode
		targetUserUUID = currentUser.ID
	}

	var req AdminUpdateUserRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	// Handle role change (admin mode only)
	if req.Role != "" && isAdminMode {
		if req.Role != "admin" && req.Role != "user" {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid role. must be 'admin' or 'user'"})
		}

		// Check if this is the last admin (preventing demotion)
		users, err := h.db.ListUsers(c.Request().Context())
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check existing users"})
		}

		adminCount := 0
		currentTargetUserRole := ""
		for _, user := range users {
			if user.Role == "admin" {
				adminCount++
			}
			if user.ID.Bytes == targetUserUUID.Bytes {
				currentTargetUserRole = user.Role
			}
		}

		// Prevent demoting the last admin
		if currentTargetUserRole == "admin" && req.Role == "user" && adminCount == 1 {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "cannot demote the last admin"})
		}

		// Update role
		_, err = h.db.UpdateUserRole(c.Request().Context(), database.UpdateUserRoleParams{
			ID:   targetUserUUID,
			Role: req.Role,
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}

		// Role changes can affect the admin count, so refresh the setup-status cache.
		setupstatus.Invalidate()
	}

	// Update username (if provided)
	if req.Username != "" {
		existingUser, err := h.db.GetUserByUsername(c.Request().Context(), req.Username)
		if err == nil && existingUser.ID.Bytes != targetUserUUID.Bytes {
			return c.JSON(http.StatusConflict, map[string]string{"error": "username already taken"})
		}

		err = h.db.UpdateUsername(c.Request().Context(), database.UpdateUsernameParams{
			ID:       targetUserUUID,
			Username: req.Username,
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
	}

	// Update timezone
	if req.Timezone != "" {
		if _, err := time.LoadLocation(req.Timezone); err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{
				"error": "Invalid timezone",
			})
		}
		err := h.db.UpdateUserTimezone(c.Request().Context(), database.UpdateUserTimezoneParams{
			ID:       targetUserUUID,
			Timezone: pgtype.Text{String: req.Timezone, Valid: true},
		})
		if err != nil {
			return err
		}
	}

	// Update email (if provided)
	if req.Email != "" {
		existingUser, err := h.db.GetUserByEmail(c.Request().Context(), req.Email)
		if err == nil && existingUser.ID.Bytes != targetUserUUID.Bytes {
			return c.JSON(http.StatusConflict, map[string]string{"error": "email already taken"})
		}

		err = h.db.UpdateEmail(c.Request().Context(), database.UpdateEmailParams{
			ID:    targetUserUUID,
			Email: req.Email,
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
	}

	// Update first/last name (if provided)
	if req.FirstName != "" || req.LastName != "" {
		err := h.db.UpdateUserProfile(c.Request().Context(), database.UpdateUserProfileParams{
			ID:        targetUserUUID,
			FirstName: pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""},
			LastName:  pgtype.Text{String: req.LastName, Valid: req.LastName != ""},
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
	}

	// Update theme (if provided)
	if req.Theme != "" {
		err := h.db.UpdateUserTheme(c.Request().Context(), database.UpdateUserThemeParams{
			ID:    targetUserUUID,
			Theme: pgtype.Text{String: req.Theme, Valid: true},
		})
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
	}

	return c.JSON(http.StatusOK, map[string]string{"message": "profile updated successfully"})
}

// ListUsers handles GET /api/auth/users
func (h *AuthHandler) ListUsers(c *echo.Context) error {
	users, err := h.db.ListUsers(c.Request().Context())
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}
	return c.JSON(http.StatusOK, users)
}

// normalizePath cleans and normalizes folder paths for consistent storage and comparison
func normalizePath(path string) string {
	fmt.Printf("normalizePath input: '%s'\n", path)

	var cleaned string
	// Handle home directory expansion (~)
	if strings.HasPrefix(path, "~/") {
		// Keep the original path for ~ to preserve user's formatting
		// Just normalize separators and that's it
		cleaned = strings.ReplaceAll(path, "\\", "/")
	} else {
		// Clean the path to remove redundant separators, ., .. etc.
		cleaned = filepath.Clean(path)
		// Convert to consistent path separators (use forward slashes for storage)
		cleaned = strings.ReplaceAll(cleaned, "\\", "/")
		// Remove trailing slash unless it's root path
		if len(cleaned) > 1 && strings.HasSuffix(cleaned, "/") {
			cleaned = strings.TrimSuffix(cleaned, "/")
		}
	}

	fmt.Printf("normalizePath output: '%s'\n", cleaned)
	return cleaned
}

type UpdateThemeRequest struct {
	Theme string `json:"theme" validate:"required"`
}

// UpdateTheme handles PUT /api/auth/theme
func (h *AuthHandler) UpdateTheme(c *echo.Context) error {
	user := MustGetAuthenticatedUser(c)

	var req UpdateThemeRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	err := h.db.UpdateUserTheme(c.Request().Context(), database.UpdateUserThemeParams{
		ID:    user.ID,
		Theme: pgtype.Text{String: req.Theme, Valid: req.Theme != ""},
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, map[string]string{"message": "theme updated successfully"})
}

type UpdateUsernameRequest struct {
	Username string `json:"username" validate:"required,min=3,max=50"`
}

// UpdateUsername handles PUT /api/auth/username
func (h *AuthHandler) UpdateUsername(c *echo.Context) error {
	user := MustGetAuthenticatedUser(c)

	var req UpdateUsernameRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	// Check if username is already taken by another user
	existingUser, err := h.db.GetUserByUsername(c.Request().Context(), req.Username)
	if err == nil && existingUser.ID.Bytes != user.ID.Bytes {
		return c.JSON(http.StatusConflict, map[string]string{"error": "username already taken"})
	}

	// Update username
	err = h.db.UpdateUsername(c.Request().Context(), database.UpdateUsernameParams{
		ID:       user.ID,
		Username: req.Username,
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, map[string]string{"message": "username updated successfully"})
}

type UpdateEmailRequest struct {
	Email string `json:"email" validate:"required,email"`
}

// UpdateEmail handles PUT /api/auth/email
func (h *AuthHandler) UpdateEmail(c *echo.Context) error {
	user := MustGetAuthenticatedUser(c)

	var req UpdateEmailRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	// Check if email is already taken by another user
	existingUser, err := h.db.GetUserByEmail(c.Request().Context(), req.Email)
	if err == nil && existingUser.ID.Bytes != user.ID.Bytes {
		return c.JSON(http.StatusConflict, map[string]string{"error": "email already taken"})
	}

	// Update email
	err = h.db.UpdateEmail(c.Request().Context(), database.UpdateEmailParams{
		ID:    user.ID,
		Email: req.Email,
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, map[string]string{"message": "email updated successfully"})
}

type UpdatePasswordRequest struct {
	CurrentPassword string `json:"current_password" validate:"required"`
	NewPassword     string `json:"new_password" validate:"required,passwordcomplex"`
	ConfirmPassword string `json:"confirm_password" validate:"required"`
}

// UpdatePassword handles PUT /api/auth/password (self-change) and PUT /api/auth/password/:id (admin reset)
// Combined handler for both self-service password change and admin password reset
func (h *AuthHandler) UpdatePassword(c *echo.Context) error {
	currentUser := MustGetAuthenticatedUser(c)

	// Determine target user: URL param (admin mode) or current user (self-change)
	targetUserID := c.Param("id")
	var targetUserUUID pgtype.UUID
	isAdminMode := targetUserID != ""

	if isAdminMode {
		// Admin resetting another user's password - validate admin role
		if currentUser.Role != "admin" {
			return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
		}
		parsedUUID, err := uuid.Parse(targetUserID)
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
		}
		targetUserUUID = pgtype.UUID{Bytes: parsedUUID, Valid: true}
	} else {
		// Self-change mode
		targetUserUUID = currentUser.ID
	}

	type PasswordRequest struct {
		CurrentPassword string `json:"current_password,omitempty" form:"current_password"`
		NewPassword     string `json:"new_password" form:"new_password" validate:"required,passwordcomplex"`
		ConfirmPassword string `json:"confirm_password" form:"confirm_password" validate:"required"`
	}

	var req PasswordRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}
	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	// Self-change mode: require current password
	if !isAdminMode {
		if req.CurrentPassword == "" {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "current password required"})
		}

		// Get current password hash
		passwordHash, err := h.db.GetUserPasswordHash(c.Request().Context(), targetUserUUID)
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": "user not found"})
		}

		// Verify current password
		err = bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.CurrentPassword))
		if err != nil {
			return c.JSON(http.StatusUnauthorized, map[string]string{"error": "current password is incorrect"})
		}
	}

	// Validate new password matches confirmation
	if req.NewPassword != req.ConfirmPassword {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "passwords do not match"})
	}

	// Hash new password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
	}

	// Update password
	err = h.db.UpdatePassword(c.Request().Context(), database.UpdatePasswordParams{
		ID:           targetUserUUID,
		PasswordHash: string(hashedPassword),
	})
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return c.JSON(http.StatusOK, map[string]string{"message": "password updated successfully"})
}

// DeleteUser handles DELETE /api/auth/profile (self-deletion) and DELETE /api/auth/profile/:id (admin deletion)
// Combined handler for both self-deletion and admin deletion of users
func (h *AuthHandler) DeleteUser(c *echo.Context) error {
	currentUser := MustGetAuthenticatedUser(c)

	// Get target user ID from URL param (admin mode) or use current user (self-deletion)
	targetUserID := c.Param("id")
	var targetUserUUID pgtype.UUID

	if targetUserID != "" {
		// Admin deletion mode
		if currentUser.Role != "admin" {
			return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
		}
		parsedUUID, err := uuid.Parse(targetUserID)
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
		}
		targetUserUUID = pgtype.UUID{Bytes: parsedUUID, Valid: true}
	} else {
		// Self-deletion mode
		targetUserUUID = currentUser.ID
	}

	// Check if this is the last admin user - prevent deletion
	users, err := h.db.ListUsers(c.Request().Context())
	if err != nil {
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check existing users"})
	}

	// Count admin users and identify the user to be deleted
	adminCount := 0
	targetUserRole := ""
	userFound := false

	for _, user := range users {
		if user.Role == "admin" {
			adminCount++
		}
		// Find target user details
		if user.ID.Bytes == targetUserUUID.Bytes {
			targetUserRole = user.Role
			userFound = true
			// Can't break here - still need to count all admins for last admin check
		}
	}

	// Check if target user exists in the database
	if !userFound {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusNotFound, `<div class="text-red-500">User not found</div>`)
		}
		return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
	}

	// Prevent deletion if target user is admin and this is the last admin
	if targetUserRole == "admin" && adminCount == 1 {
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusBadRequest, `<div class="text-red-500">Cannot delete the last admin account</div>`)
		}
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "cannot delete the last admin account"})
	}

	// If deleting an admin, reassign their libraries and media items to another admin
	// before deletion to prevent ON DELETE SET NULL from orphaning ownership
	if targetUserRole == "admin" {
		successor, err := h.db.GetFirstAdminExclude(c.Request().Context(), targetUserUUID)
		if err == nil {
			_ = h.db.ReassignLibraries(c.Request().Context(), database.ReassignLibrariesParams{
				CreatedByAdminID:   targetUserUUID,
				CreatedByAdminID_2: successor.ID,
			})
			_ = h.db.ReassignMediaItems(c.Request().Context(), database.ReassignMediaItemsParams{
				AddedByAdminID:   targetUserUUID,
				AddedByAdminID_2: successor.ID,
			})
		}
	}

	// Delete user (this will cascade to delete all related data)
	err = h.db.DeleteUser(c.Request().Context(), targetUserUUID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			if c.Request().Header.Get("HX-Request") == "true" {
				return c.HTML(http.StatusNotFound, `<div class="text-red-500">User not found</div>`)
			}
			return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
		}
		if c.Request().Header.Get("HX-Request") == "true" {
			return c.HTML(http.StatusInternalServerError, `<div class="text-red-500">Failed to delete account</div>`)
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	// Deletion may have changed the admin count, so refresh the setup-status cache.
	setupstatus.Invalidate()

	// Create success message based on context
	var message string
	if targetUserID != "" && targetUserUUID.Bytes != currentUser.ID.Bytes {
		message = "user account deleted successfully"
	} else {
		message = "account deleted successfully"
	}

	return c.JSON(http.StatusOK, map[string]string{"message": message})
}

type UpdateUserMaxDevicesRequest struct {
	MaxDevices int32 `json:"max_devices" validate:"required,min=1,max=100"`
}

// UpdateUserMaxDevices handles PUT /api/auth/users/:id/max-devices (admin only)
func (h *AuthHandler) UpdateUserMaxDevices(c *echo.Context) error {
	userID := c.Param("id")
	if userID == "" {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "user id required"})
	}

	var req UpdateUserMaxDevicesRequest
	if err := c.Bind(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
	}

	if err := c.Validate(&req); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
	}

	userUUID, err := uuid.Parse(userID)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
	}

	user, err := h.db.UpdateUserMaxDevices(c.Request().Context(), database.UpdateUserMaxDevicesParams{
		ID:         pgtype.UUID{Bytes: userUUID, Valid: true},
		MaxDevices: pgtype.Int4{Int32: req.MaxDevices, Valid: true},
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
		}
		return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	_ = user // Suppress unused variable warning when sqlc returns user
	return c.JSON(http.StatusOK, map[string]string{"message": "max devices updated"})
}

// AdminMiddleware checks if the user has admin role
func AdminMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c *echo.Context) error {
		userRole, exists := c.Get("user_role").(string)
		if !exists || userRole != "admin" {
			return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
		}
		return next(c)
	}
}

func (h *AuthHandler) generateJWTWithAllClaims(userID, userRole, userEmail, userUsername string) (string, error) {
	claims := jwt.MapClaims{
		"jti":           uuid.New().String(),
		"user_id":       userID,
		"user_role":     userRole,
		"user_email":    userEmail,
		"user_username": userUsername,
		"exp":           time.Now().Add(h.sessionDuration()).Unix(),
		"iat":           time.Now().Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(h.jwtKey)
}

func (h *AuthHandler) generateJWTWithRole(userID, userRole string) (string, error) {
	return h.generateJWTWithAllClaims(userID, userRole, "", "")
}

func (h *AuthHandler) generateJWT(userID string) (string, error) {
	return h.generateJWTWithRole(userID, "user")
}

func (h *AuthHandler) CreateDefaultCollectionsForUser(ctx context.Context, userID pgtype.UUID) error {
	defaultCollections := []struct {
		Name        string
		Description string
		Icon        string
		Color       string
		QueryType   string
		Priority    int32
	}{
		{"Continue Reading", "Books you're currently reading (0 < progress < 1)", "📖", "#7aa2f7", "continue-reading", 1},
		{"Recently Added", "Newly added items to this library", "🆕", "#9ece6a", "recently-added", 2},
		{"Recently Read", "Books you've finished (progress >= 1)", "✅", "#e0af68", "recently-read", 3},
		{"Not Started", "Books you haven't read yet (progress = 0 or no record)", "📕", "#f7768e", "not-started", 4},
		{"Continue Series", "Next book in series you're reading", "📚", "#bb9af7", "continue-series", 5},
	}

	for _, col := range defaultCollections {
		_, err := h.db.CreateSystemCollection(ctx, database.CreateSystemCollectionParams{
			UserID:          userID,
			Name:            col.Name,
			Description:     pgtype.Text{String: col.Description, Valid: true},
			Icon:            pgtype.Text{String: col.Icon, Valid: true},
			Color:           pgtype.Text{String: col.Color, Valid: true},
			ShowOnDashboard: pgtype.Bool{Bool: true, Valid: true},
			QueryType:       pgtype.Text{String: col.QueryType, Valid: true},
			Priority:        pgtype.Int4{Int32: col.Priority, Valid: true},
		})
		if err != nil {
			return err
		}
	}
	return nil
}
