package user

import (
	"bytes"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"log/slog"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/kovidgoyal/imaging"
	"golang.org/x/crypto/bcrypt"
	"golang.org/x/text/cases"
	"golang.org/x/text/language"

	"crussell/clock"
	"crussell/db"
	"crussell/handlers/auth"
	"crussell/internal/images"
	"crussell/internal/s3"
	"crussell/internal/validators"
	"crussell/mw"
	"github.com/jackc/pgx/v5"
)

func getEnv(key, fallback string) string {
	if val := os.Getenv(key); val != "" {
		return val
	}
	return fallback
}

var titleCaser = cases.Title(language.English)

type UserProfile struct {
	ID                string  `json:"id"`
	Email             string  `json:"email"`
	FirstName         string  `json:"firstName"`
	LastName          string  `json:"lastName"`
	Phone             *string `json:"phone,omitempty"`
	DateOfBirth       *string `json:"dateOfBirth,omitempty"`
	Role              string  `json:"role"`
	LoyaltyStamps     int     `json:"loyaltyStamps"`
	ReferralCode      string  `json:"referralCode"`
	ReferralCodeUses  int     `json:"referralCodeUses"`
	ReferralSavings   float64 `json:"referralSavings"`
	ProfilePicURL     *string `json:"profilePicUrl,omitempty"`
	DepositsRequired  int     `json:"deposits_required"`
	PreviousFirstName *string `json:"previousFirstName,omitempty"`
	PreviousLastName  *string `json:"previousLastName,omitempty"`
}

type UpdateProfileRequest struct {
	FirstName string `json:"firstName" validate:"required,min=1,max=50"`
	LastName  string `json:"lastName"  validate:"required,min=1,max=50"`
	Phone     string `json:"phone"     validate:"required"`
}

type AdminUserDetail struct {
	ID               string  `json:"id"`
	Email            *string `json:"email,omitempty"`
	FirstName        string  `json:"firstName"`
	LastName         string  `json:"lastName"`
	FullName         string  `json:"fullName"`
	Phone            *string `json:"phone,omitempty"`
	DateOfBirth      *string `json:"dateOfBirth,omitempty"`
	ProfilePicURL    *string `json:"profilePicUrl,omitempty"`
	AccountRole      string  `json:"accountRole"`
	AccountType      string  `json:"accountType"`
	LoyaltyStamps    int     `json:"loyaltyStamps"`
	ReferralCode     string  `json:"referralCode"`
	ReferralCodeUses int     `json:"referralCodeUses"`
	LastLoginAt      *string `json:"lastLoginAt,omitempty"`
	CreatedAt        string  `json:"createdAt"`
	UpdatedAt        string  `json:"updatedAt"`
	Notes            *string `json:"notes,omitempty" validate:"omitempty,max=1000000"`

	// GDPR consent fields
	PrivacyPolicyConsent   bool    `json:"privacyPolicyConsent"`
	PolicyConsentUpdatedAt *string `json:"policyConsentUpdatedAt,omitempty"`
	DataRetentionConsent   bool    `json:"dataRetentionConsent"`
	DataConsentUpdatedAt   *string `json:"dataConsentUpdatedAt,omitempty"`

	// Social logins
	SocialLogins []SocialLogin `json:"socialLogins,omitempty"`

	// Name change history
	PreviousFirstName *string `json:"previousFirstName,omitempty"`
	PreviousLastName  *string `json:"previousLastName,omitempty"`
}

type SocialLogin struct {
	Provider  string `json:"provider"`
	CreatedAt string `json:"createdAt"`
}

type UserListItem struct {
	ID                string    `json:"id"`
	FullName          string    `json:"fullName"`
	Email             *string   `json:"email,omitempty"`
	Phone             *string   `json:"phone,omitempty"`
	AccountRole       string    `json:"account_role"`
	CreatedAt         time.Time `json:"created_at"`
	PreviousFirstName *string   `json:"previousFirstName,omitempty"`
	PreviousLastName  *string   `json:"previousLastName,omitempty"`
	CompletedCount    int       `json:"completed_count"`
}

type UserListResponse struct {
	Users      []UserListItem `json:"users"`
	Total      int            `json:"total"`
	Page       int            `json:"page"`
	PerPage    int            `json:"perPage"`
	TotalPages int            `json:"totalPages"`
	NextCursor *string        `json:"next_cursor,omitempty"`
}

// GET /api/user/profile
func GetProfileHandler(w http.ResponseWriter, r *http.Request) {
	userID, ok := mw.GetUserID(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var user UserProfile
	err := db.Conn.QueryRow(r.Context(), `
		SELECT
            id, email, n_first_name, n_last_name, phone,
            date_of_birth::text, account_role, loyalty_stamps,
            referral_code, profile_pic_url, deposits_required,
            (SELECT COUNT(*) FROM user_referrals WHERE referrer_id = users.id AND claimed_booking_id IS NOT NULL) AS referral_code_uses,
            (SELECT COALESCE(SUM(bd.discount_amount), 0) FROM booking_discounts bd WHERE bd.user_id = users.id AND bd.discount_source = 'referral') AS referral_savings
        FROM users
        WHERE id = $1
    `, userID).Scan(
		&user.ID, &user.Email, &user.FirstName, &user.LastName,
		&user.Phone, &user.DateOfBirth, &user.Role,
		&user.LoyaltyStamps, &user.ReferralCode, &user.ProfilePicURL, &user.DepositsRequired, &user.ReferralCodeUses, &user.ReferralSavings,
	)

	if err != nil {
		http.Error(w, "user not found", http.StatusNotFound)
		return
	}

	// Check if user has an unconsumed previous name (booking_id IS NULL means the name
	// change hasn't been "seen" via a completed booking yet).
	var prevFirstName, prevLastName sql.NullString
	err = db.Conn.QueryRow(r.Context(), `
		SELECT previous_first_name, previous_last_name
		FROM name_history
		WHERE user_id = $1 AND booking_id IS NULL
		ORDER BY changed_at ASC
		LIMIT 1
	`, userID).Scan(&prevFirstName, &prevLastName)
	if err == nil && prevFirstName.Valid && prevLastName.Valid {
		if prevFirstName.String != user.FirstName || prevLastName.String != user.LastName {
			user.PreviousFirstName = &prevFirstName.String
			user.PreviousLastName = &prevLastName.String
		}
	}

	_ = json.NewEncoder(w).Encode(user)
}

// PUT /api/user/profile
// updateCardDAV updates an existing contact in SabreDAV using user ID.
// The DAV base URL is configured via DAV_BASE_URL — if unset, the update is skipped
// silently (allowing the handler to work in dev environments without a DAV server).
func updateCardDAV(userID, firstName, lastName, email, phone, dob, profilePicURL string) error {
	davBase := os.Getenv("DAV_BASE_URL")
	if davBase == "" {
		return nil
	}
	filename := fmt.Sprintf("%s.vcf", userID)
	url := fmt.Sprintf("%s/addressbooks/principals/default/default/%s", davBase, filename)

	timestamp := clock.Now().UTC().Format("20060102T150405Z")
	uid := fmt.Sprintf("%s@example.com", userID)

	var photoLine string
	if profilePicURL != "" {
		photoLine = fmt.Sprintf("PHOTO;VALUE=URI:%s", profilePicURL)
	}

	vcard := fmt.Sprintf(`BEGIN:VCARD
VERSION:3.0
UID:%s
FN:%s %s
N:%s;%s;;;
EMAIL;TYPE=INTERNET:%s
TEL;TYPE=CELL:%s
BDAY:%s
%s
REV:%s
END:VCARD`, uid, firstName, lastName, lastName, firstName, email, phone, dob, photoLine, timestamp)

	// PUT updated vCard
	req, err := http.NewRequest("PUT", url, bytes.NewBufferString(vcard))
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Content-Type", "text/vcard; charset=utf-8")
	davPassword := os.Getenv("DAV_ADMIN_PASSWORD")
	if davPassword == "" {
		davPassword = "admin"
	}
	req.SetBasicAuth("admin", davPassword)

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("failed to update CardDAV: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("CardDAV returned status: %d", resp.StatusCode)
	}

	return nil
}

// PUT /api/user/profile
func UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
	userID, _ := mw.GetUserID(r.Context())

	var req UpdateProfileRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}

	if err := validators.Validate.Struct(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Normalize input
	req.FirstName = strings.TrimSpace(req.FirstName)
	req.LastName = strings.TrimSpace(req.LastName)
	req.Phone = strings.TrimSpace(req.Phone)

	// Required fields
	if req.FirstName == "" || req.LastName == "" || req.Phone == "" {
		http.Error(w, "first name, last name and phone are required", http.StatusBadRequest)
		return
	}

	// Validate names (unicode letters, spaces, hyphen, apostrophe, dot)
	nameRegex := regexp.MustCompile(`^[\p{L}\p{M}\s\-'\.]+$`)

	if !nameRegex.MatchString(req.FirstName) || !nameRegex.MatchString(req.LastName) {
		http.Error(w, "invalid characters in name", http.StatusBadRequest)
		return
	}

	// Validate lengths
	if len(req.FirstName) < 1 || len(req.FirstName) > 50 {
		http.Error(w, "first name must be 1-50 characters", http.StatusBadRequest)
		return
	}
	if len(req.LastName) < 1 || len(req.LastName) > 50 {
		http.Error(w, "last name must be 1-50 characters", http.StatusBadRequest)
		return
	}

	// Normalize phone (strip spaces, hyphens, brackets)
	req.Phone = strings.Map(func(r rune) rune {
		if (r >= '0' && r <= '9') || r == '+' {
			return r
		}
		return -1
	}, req.Phone)

	// Validate UK phone number
	phone, err := auth.ValidateUKPhoneNumber(req.Phone)
	if err != nil {
		http.Error(w, "invalid phone number format", http.StatusBadRequest)
		return
	}
	req.Phone = strings.TrimSpace(phone)

	// Title case names
	req.FirstName = titleCaser.String(strings.ToLower(req.FirstName))
	req.LastName = titleCaser.String(strings.ToLower(req.LastName))

	// Fetch user's current data for name change detection and CardDAV update
	var email string
	var dob sql.NullTime
	var profilePicURL sql.NullString
	var currentFirstName, currentLastName string
	err = db.Conn.QueryRow(r.Context(), `
		SELECT email, date_of_birth, profile_pic_url, n_first_name, n_last_name FROM users WHERE id = $1
	`, userID).Scan(&email, &dob, &profilePicURL, &currentFirstName, &currentLastName)

	if err != nil {
		log.Printf("Failed to fetch user %s: %v", userID, err)
		http.Error(w, "failed to fetch user data", http.StatusInternalServerError)
		return
	}

	// Use a transaction for atomicity: insert name history + update user
	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		http.Error(w, "failed to begin transaction", http.StatusInternalServerError)
		return
	}
	defer func() {
		if err := tx.Rollback(r.Context()); err != nil {
			slog.Error("failed to rollback transaction", "err", err)
		}
	}()

	// If first or last name changed (via user edit), track the old names in history
	nameChanged := currentFirstName != req.FirstName || currentLastName != req.LastName
	if nameChanged {
		_, err = tx.Exec(r.Context(), `
			INSERT INTO name_history (user_id, previous_first_name, previous_last_name)
			VALUES ($1, $2, $3)
		`, userID, currentFirstName, currentLastName)
		if err != nil {
			log.Printf("Failed to insert name history for user %s: %v", userID, err)
			http.Error(w, "failed to record name change", http.StatusInternalServerError)
			return
		}
	}

	// Update DB
	_, err = tx.Exec(r.Context(), `
		UPDATE users
		SET n_first_name = $1, n_last_name = $2, phone = $3, updated_at = NOW()
		WHERE id = $4
	`, req.FirstName, req.LastName, req.Phone, userID)

	if err != nil {
		http.Error(w, "update failed", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		http.Error(w, "failed to commit transaction", http.StatusInternalServerError)
		return
	}

	// Update CardDAV (non-blocking)
	go func() {
		defer func() {
			if r := recover(); r != nil {
				log.Printf("Panic recovered in CardDAV profile update: %v", r)
			}
		}()
		var dobStr string
		if dob.Valid {
			dobStr = dob.Time.Format("2006-01-02")
		}
		if err := updateCardDAV(userID, req.FirstName, req.LastName, email, req.Phone, dobStr, profilePicURL.String); err != nil {
			fmt.Printf("Warning: Failed to update CardDAV contact for user %s: %v\n", userID, err)
		}
	}()

	w.WriteHeader(http.StatusOK)
}

// GET /api/admin/users/{id}
func GetAdminUserHandler(w http.ResponseWriter, r *http.Request) {
	userID := chi.URLParam(r, "id")
	if userID == "" || !validators.IsValidID(userID) {
		http.Error(w, "user not found", http.StatusNotFound)
		return
	}

	// Fetch user details
	var user AdminUserDetail
	err := db.Conn.QueryRow(r.Context(), `
		SELECT
			id, email, n_first_name, n_last_name, fn, phone,
			date_of_birth::text, profile_pic_url,
			account_role, account_type, loyalty_stamps, referral_code,
			last_login_at::text, created_at::text, updated_at::text, notes,
			privacy_policy_and_terms_consent,
			policy_consent_updated_at::text,
			data_retention_consent,
			data_consent_updated_at::text
		FROM users
		WHERE id = $1
	`, userID).Scan(
		&user.ID, &user.Email, &user.FirstName, &user.LastName, &user.FullName,
		&user.Phone, &user.DateOfBirth, &user.ProfilePicURL,
		&user.AccountRole, &user.AccountType, &user.LoyaltyStamps, &user.ReferralCode,
		&user.LastLoginAt, &user.CreatedAt, &user.UpdatedAt, &user.Notes,
		&user.PrivacyPolicyConsent, &user.PolicyConsentUpdatedAt,
		&user.DataRetentionConsent, &user.DataConsentUpdatedAt,
	)

	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.Error(w, "user not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to fetch user %s: %v", userID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Fetch referral code uses count
	err = db.Conn.QueryRow(r.Context(), `
		SELECT COUNT(*)
		FROM user_referrals
		WHERE referrer_id = $1 AND claimed_booking_id IS NOT NULL
	`, userID).Scan(&user.ReferralCodeUses)
	if err != nil {
		log.Printf("Failed to fetch referral code uses for user %s: %v", userID, err)
		user.ReferralCodeUses = 0
	}

	// Fetch unconsumed name history (booking_id IS NULL = not yet "seen" via a completed booking)
	var prevFirstName, prevLastName sql.NullString
	err = db.Conn.QueryRow(r.Context(), `
		SELECT nh.previous_first_name, nh.previous_last_name
		FROM name_history nh
		WHERE nh.user_id = $1 AND nh.booking_id IS NULL
		ORDER BY nh.changed_at ASC
		LIMIT 1
	`, userID).Scan(&prevFirstName, &prevLastName)
	if err == nil && prevFirstName.Valid && prevLastName.Valid {
		if prevFirstName.String != user.FirstName || prevLastName.String != user.LastName {
			user.PreviousFirstName = &prevFirstName.String
			user.PreviousLastName = &prevLastName.String
		}
	}

	// Fetch social logins
	socialRows, err := db.Conn.Query(r.Context(), `
		SELECT provider, created_at::text
		FROM user_social_logins
		WHERE user_id = $1
		ORDER BY created_at ASC
	`, userID)
	if err != nil {
		log.Printf("Failed to fetch social logins for user %s: %v", userID, err)
	} else {
		defer socialRows.Close()

		var socialLogins []SocialLogin
		for socialRows.Next() {
			var sl SocialLogin
			if err := socialRows.Scan(&sl.Provider, &sl.CreatedAt); err != nil {
				log.Printf("Failed to scan social login for user %s: %v", userID, err)
				continue
			}
			socialLogins = append(socialLogins, sl)
		}

		if len(socialLogins) > 0 {
			user.SocialLogins = socialLogins
		}
	}

	w.WriteHeader(http.StatusOK)
	if err := json.NewEncoder(w).Encode(user); err != nil {
		log.Printf("Failed to encode user response: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
}

// parseCursor splits a "createdAt|id" cursor string into its components.

// GET /api/admin/users
func ListAdminUsersHandler(w http.ResponseWriter, r *http.Request) {
	// Parse query parameters
	query := r.URL.Query()
	searchTerm := query.Get("q")
	cursorStr := query.Get("cursor")

	// Pagination parameters
	perPage := 10

	if perPageStr := query.Get("per_page"); perPageStr != "" {
		if pp, err := strconv.Atoi(perPageStr); err == nil && pp > 0 && pp <= 100 {
			perPage = pp
		}
	}

	page := 1
	if pageStr := query.Get("page"); pageStr != "" {
		if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
			page = p
		}
	}

	innerQuery := `
        SELECT u.id, u.fn, u.email, u.phone, u.account_role, u.created_at,
               nh.previous_first_name, nh.previous_last_name,
               (SELECT COUNT(*) FROM bookings b WHERE b.user_id = u.id AND b.status = 'completed') AS completed_count
        FROM users u
        LEFT JOIN LATERAL (
            SELECT previous_first_name, previous_last_name
            FROM name_history
            WHERE user_id = u.id AND booking_id IS NULL
            ORDER BY changed_at ASC
            LIMIT 1
        ) nh ON true
    `

	var listQuery string
	var listArgs []any

	if searchTerm != "" {
		searchPattern := "%" + searchTerm + "%"

		innerWithWhere := `SELECT id, fn, email, phone, account_role, created_at, previous_first_name, previous_last_name, completed_count FROM (` + innerQuery + `
            WHERE (u.fn ILIKE $1 OR u.email ILIKE $1 OR u.phone ILIKE $1)
        ) sub`

		listArgs = []any{searchPattern}

		if cursorStr != "" {
			cursorCount, cursorCreatedAt, cursorID, err := validators.ParseCursor3(cursorStr)
			if err != nil {
				http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest)
				return
			}
			listQuery = innerWithWhere + ` WHERE (completed_count, created_at, id) < ($2, $3, $4)`
			listArgs = append(listArgs, cursorCount, cursorCreatedAt, cursorID)
		} else {
			listQuery = innerWithWhere
		}
		listQuery += ` ORDER BY completed_count DESC, created_at DESC, id DESC LIMIT $` + strconv.Itoa(len(listArgs)+1)
		listArgs = append(listArgs, perPage+1)
	} else {
		innerNoWhere := `SELECT id, fn, email, phone, account_role, created_at, previous_first_name, previous_last_name, completed_count FROM (` + innerQuery + `) sub`

		if cursorStr != "" {
			cursorCount, cursorCreatedAt, cursorID, err := validators.ParseCursor3(cursorStr)
			if err != nil {
				http.Error(w, "Invalid cursor: "+err.Error(), http.StatusBadRequest)
				return
			}
			listQuery = innerNoWhere + ` WHERE (completed_count, created_at, id) < ($1, $2, $3)`
			listArgs = append(listArgs, cursorCount, cursorCreatedAt, cursorID)
		} else {
			listQuery = innerNoWhere
		}
		listQuery += ` ORDER BY completed_count DESC, created_at DESC, id DESC LIMIT $` + strconv.Itoa(len(listArgs)+1)
		listArgs = append(listArgs, perPage+1)
	}

	var users []UserListItem
	var total int

	// Compute total with a simple count query (no ORDER BY/LIMIT/HAVING).
	// Run BEFORE the data query to avoid "conn busy" errors when routing
	// through a per-test transaction (pgx.Tx does not support concurrent queries).
	if searchTerm != "" {
		//nolint:errcheck // zero value is acceptable fallback on scan failure
		_ = db.Conn.QueryRow(r.Context(), `SELECT COUNT(DISTINCT u.id) FROM users u
			LEFT JOIN bookings b ON u.id = b.user_id
			WHERE u.fn ILIKE $1 OR u.email ILIKE $1 OR u.phone ILIKE $1`, "%"+searchTerm+"%").Scan(&total)
	} else {
		//nolint:errcheck // zero value is acceptable fallback on scan failure
		_ = db.Conn.QueryRow(r.Context(), "SELECT COUNT(*) FROM users").Scan(&total)
	}

	// Get users list
	rows, err := db.Conn.Query(r.Context(), listQuery, listArgs...)
	if err != nil {
		log.Printf("Failed to fetch users: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var user UserListItem
		var prevFirstName, prevLastName sql.NullString
		var completedCount int
		err := rows.Scan(
			&user.ID,
			&user.FullName,
			&user.Email,
			&user.Phone,
			&user.AccountRole,
			&user.CreatedAt,
			&prevFirstName,
			&prevLastName,
			&completedCount,
		)
		if err != nil {
			log.Printf("Failed to scan user row: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		if prevFirstName.Valid && prevLastName.Valid {
			user.PreviousFirstName = &prevFirstName.String
			user.PreviousLastName = &prevLastName.String
		}
		user.CompletedCount = completedCount
		users = append(users, user)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Handle empty results
	if users == nil {
		users = []UserListItem{}
	}

	var nextCursor *string
	if len(users) > perPage {
		users = users[:perPage]
		last := users[len(users)-1]
		cursor := fmt.Sprintf("%d|%s|%s", last.CompletedCount, last.CreatedAt.Format(time.RFC3339Nano), last.ID)
		nextCursor = &cursor
	}

	// Calculate total pages
	totalPages := (total + perPage - 1) / perPage
	if totalPages == 0 {
		totalPages = 1
	}

	response := UserListResponse{
		Users:      users,
		Total:      total,
		Page:       page,
		PerPage:    perPage,
		TotalPages: totalPages,
		NextCursor: nextCursor,
	}

	w.WriteHeader(http.StatusOK)
	if err := json.NewEncoder(w).Encode(response); err != nil {
		log.Printf("Failed to encode users response: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
}

type ChangePasswordRequest struct {
	CurrentPassword string `json:"current_password" validate:"required,max=72"`
	NewPassword     string `json:"new_password"     validate:"required,min=6,max=72"`
}

func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
	userID, ok := mw.GetUserID(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req ChangePasswordRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}

	if err := validators.Validate.Struct(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if req.CurrentPassword == "" || req.NewPassword == "" {
		http.Error(w, "current password and new password are required", http.StatusBadRequest)
		return
	}

	if len(req.NewPassword) < 6 {
		http.Error(w, "password must be at least 6 characters", http.StatusBadRequest)
		return
	}
	if len(req.NewPassword) > 72 {
		http.Error(w, "password must be less than 72 characters", http.StatusBadRequest)
		return
	}

	if req.NewPassword == req.CurrentPassword {
		http.Error(w, "new password must be different from current password", http.StatusBadRequest)
		return
	}

	var passwordHash string
	err := db.Conn.QueryRow(r.Context(), `SELECT password_hash FROM users WHERE id = $1`, userID).Scan(&passwordHash)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.Error(w, "user not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to fetch password hash for user %s: %v", userID, err)
		http.Error(w, "server error", http.StatusInternalServerError)
		return
	}

	if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.CurrentPassword)); err != nil {
		http.Error(w, "current password is incorrect", http.StatusUnauthorized)
		return
	}

	newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
	if err != nil {
		log.Printf("Failed to hash new password for user %s: %v", userID, err)
		http.Error(w, "server error", http.StatusInternalServerError)
		return
	}

	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
		if err := tx.Rollback(r.Context()); err != nil {
			slog.Error("failed to rollback transaction", "err", err)
		}
	}()

	_, err = tx.Exec(r.Context(), `UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2`, string(newHash), userID)
	if err != nil {
		log.Printf("Failed to update password for user %s: %v", userID, err)
		http.Error(w, "failed to update password", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Revoke all existing tokens by invalidating the current JTI for this user
	// This forces the user to re-authenticate after changing their password
	log.Printf("Password changed for user %s - existing sessions should re-authenticate", userID)

	w.WriteHeader(http.StatusOK)
}

// ServiceForPatchTest represents a service that requires a patch test
type ServiceForPatchTest struct {
	ID                  string `json:"id"`
	Name                string `json:"name"`
	PatchTestID         string `json:"patchTestId"`
	NoticeDurationHours int    `json:"noticeDurationHours"`
	ExpiryMonths        int    `json:"expiryMonths"`
}

// GET /api/admin/users/{id}/patch-tests/eligible
// Returns services that require a patch test which the user hasn't completed yet
func GetEligiblePatchTestServicesHandler(w http.ResponseWriter, r *http.Request) {
	userID := chi.URLParam(r, "id")
	if userID == "" || !validators.IsValidID(userID) {
		http.Error(w, "user not found", http.StatusNotFound)
		return
	}

	// Get services that require a patch test but user hasn't completed
	// This queries patch_tests to find which services require patch tests,
	// then excludes those the user already has valid records for
	rows, err := db.Conn.Query(r.Context(), `
		SELECT DISTINCT s.id, s.name, pt.id, pt.notice_duration_hours, pt.expiry_months
		FROM services s
		JOIN patch_tests pt ON s.id = ANY(pt.service_ids)
		WHERE s.is_active = true
		AND pt.id NOT IN (
			SELECT pt_inner.id
			FROM patch_tests pt_inner
			JOIN user_patch_tests upt ON pt_inner.id = upt.patch_test_id
			WHERE upt.user_id = $1
			AND upt.tested_at + (pt_inner.expiry_months || ' months')::interval > NOW()
		)
		ORDER BY s.name ASC
	`, userID)
	if err != nil {
		log.Printf("Failed to fetch eligible patch test services: %v", err)
		http.Error(w, "server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var services []ServiceForPatchTest
	for rows.Next() {
		var s ServiceForPatchTest
		if err := rows.Scan(&s.ID, &s.Name, &s.PatchTestID, &s.NoticeDurationHours, &s.ExpiryMonths); err != nil {
			log.Printf("Failed to scan service: %v", err)
			continue
		}
		services = append(services, s)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if services == nil {
		services = []ServiceForPatchTest{}
	}

	_ = json.NewEncoder(w).Encode(services)
}

type AddPatchTestRequest struct {
	PatchTestID string `json:"patch_test_id" validate:"required"`
}

// POST /api/admin/users/{id}/patch-tests
// Records that a user has taken a patch test
func AddPatchTestHandler(w http.ResponseWriter, r *http.Request) {
	userID := chi.URLParam(r, "id")
	if userID == "" || !validators.IsValidID(userID) {
		http.Error(w, "user not found", http.StatusNotFound)
		return
	}

	var req AddPatchTestRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}
	if err := validators.Validate.Struct(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if req.PatchTestID == "" {
		http.Error(w, "patch_test_id is required", http.StatusBadRequest)
		return
	}

	// Verify patch test exists
	var patchTestID string
	err := db.Conn.QueryRow(r.Context(), `SELECT id FROM patch_tests WHERE id = $1`, req.PatchTestID).Scan(&patchTestID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			http.Error(w, "patch test not found", http.StatusBadRequest)
			return
		}
		log.Printf("Failed to verify patch test: %v", err)
		http.Error(w, "server error", http.StatusInternalServerError)
		return
	}

	// Insert or update user_patch_tests record
	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
		if err := tx.Rollback(r.Context()); err != nil {
			slog.Error("failed to rollback transaction", "err", err)
		}
	}()

	_, err = tx.Exec(r.Context(), `
		INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
		VALUES ($1, $2, NOW())
		ON CONFLICT (user_id, patch_test_id) DO UPDATE SET tested_at = NOW()
	`, userID, req.PatchTestID)
	if err != nil {
		log.Printf("Failed to add patch test: %v", err)
		http.Error(w, "failed to add patch test", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusCreated)
}

// UserPatchTest represents a user's patch test record
type UserPatchTest struct {
	ID            string    `json:"id"`
	PatchTestID   string    `json:"patchTestId"`
	PatchTestName string    `json:"patchTestName"`
	TestedAt      time.Time `json:"testedAt"`
	ValidUntil    time.Time `json:"validUntil"`
}

// GET /api/admin/users/{user_id}/patch-tests
// Returns all patch test records for a user
func GetUserPatchTestsHandler(w http.ResponseWriter, r *http.Request) {
	userID := chi.URLParam(r, "user_id")
	if userID == "" || !validators.IsValidID(userID) {
		http.Error(w, "user not found", http.StatusNotFound)
		return
	}

	rows, err := db.Conn.Query(r.Context(), `
		SELECT upt.id, upt.patch_test_id, pt.name, upt.tested_at, upt.tested_at + (pt.expiry_months || ' months')::interval as valid_until
		FROM user_patch_tests upt
		JOIN patch_tests pt ON upt.patch_test_id = pt.id
		WHERE upt.user_id = $1
		ORDER BY upt.tested_at DESC
	`, userID)
	if err != nil {
		log.Printf("Failed to get patch tests: %v", err)
		http.Error(w, "server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var tests []UserPatchTest
	for rows.Next() {
		var t UserPatchTest
		if err := rows.Scan(&t.ID, &t.PatchTestID, &t.PatchTestName, &t.TestedAt, &t.ValidUntil); err != nil {
			log.Printf("Failed to scan patch test: %v", err)
			continue
		}
		tests = append(tests, t)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	_ = json.NewEncoder(w).Encode(tests)
}

// DELETE /api/admin/users/{user_id}/patch-tests/{test_id}
func DeletePatchTestHandler(w http.ResponseWriter, r *http.Request) {
	userID := chi.URLParam(r, "user_id")
	testID := chi.URLParam(r, "test_id")
	if userID == "" || !validators.IsValidID(userID) {
		http.Error(w, "user not found", http.StatusNotFound)
		return
	}
	if testID == "" || !validators.IsValidID(testID) {
		http.Error(w, "Patch test not found", http.StatusNotFound)
		return
	}

	// testID in this context is the user_patch_tests.id (CHAR(12) hex)
	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
		if err := tx.Rollback(r.Context()); err != nil {
			slog.Error("failed to rollback transaction", "err", err)
		}
	}()

	result, err := tx.Exec(r.Context(), `
		DELETE FROM user_patch_tests WHERE id = $1 AND user_id = $2
	`, testID, userID)
	if err != nil {
		log.Printf("Failed to delete patch test: %v", err)
		http.Error(w, "server error", http.StatusInternalServerError)
		return
	}

	if result.RowsAffected() == 0 {
		http.Error(w, "patch test not found", http.StatusNotFound)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

type UploadProfilePicResponse struct {
	URL string `json:"url"`
}

func UploadProfilePictureHandler(w http.ResponseWriter, r *http.Request) {
	userID, ok := r.Context().Value(mw.UserIDKey).(string)
	if !ok || userID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	if s3.Client == nil {
		log.Printf("S3 client not initialized")
		http.Error(w, "Storage not configured", http.StatusInternalServerError)
		return
	}

	//nolint:errcheck // parse errors are non-fatal; form values may still be available
	_ = r.ParseMultipartForm(10 << 20)

	file, _, err := r.FormFile("file")
	if err != nil {
		log.Printf("Failed to get file: %v", err)
		http.Error(w, "No file provided", http.StatusBadRequest)
		return
	}
	defer file.Close()

	fileBytes, err := io.ReadAll(file)
	if err != nil {
		log.Printf("Failed to read file: %v", err)
		http.Error(w, "Failed to read file", http.StatusBadRequest)
		return
	}

	if _, err := images.ValidateImageBytes(fileBytes); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	ext := ".jpg"
	key := fmt.Sprintf("profiles/%s%s", userID, ext)
	bucket := getEnv("S3_PROFILE_PICS_BUCKET", "crussell-profile-pics")

	var oldURL sql.NullString
	err = db.Conn.QueryRow(r.Context(), `SELECT profile_pic_url FROM users WHERE id = $1`, userID).Scan(&oldURL)
	if err == nil && oldURL.Valid && oldURL.String != "" {
		if delErr := s3.Client.Delete(r.Context(), bucket, key); delErr != nil {
			log.Printf("Warning: Failed to delete old profile picture for user %s: %v", userID, delErr)
		}
	}

	fileBytes, err = processProfileImage(fileBytes)
	if err != nil {
		log.Printf("Failed to process image: %v", err)
		http.Error(w, "Failed to process image", http.StatusInternalServerError)
		return
	}

	if err := s3.Client.Upload(r.Context(), bucket, key, bytes.NewReader(fileBytes), "image/jpeg"); err != nil {
		log.Printf("Failed to upload profile picture to S3: %v", err)
		http.Error(w, "Failed to upload image", http.StatusInternalServerError)
		return
	}

	url, err := s3.Client.GetURL(r.Context(), bucket, key)
	if err != nil {
		log.Printf("Failed to get URL: %v", err)
		http.Error(w, "Failed to get image URL", http.StatusInternalServerError)
		return
	}

	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
		if err := tx.Rollback(r.Context()); err != nil {
			slog.Error("failed to rollback transaction", "err", err)
		}
	}()

	_, err = tx.Exec(r.Context(), `UPDATE users SET profile_pic_url = $1 WHERE id = $2`, url, userID)
	if err != nil {
		log.Printf("Failed to update user profile pic: %v", err)
		http.Error(w, "Failed to save profile picture", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	_ = json.NewEncoder(w).Encode(UploadProfilePicResponse{URL: url})
}

func processProfileImage(data []byte) ([]byte, error) {
	img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true))
	if err != nil {
		return nil, fmt.Errorf("failed to decode image: %w", err)
	}

	img = imaging.Thumbnail(img, 350, 350, imaging.Linear)

	var buf bytes.Buffer
	err = imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(85))
	if err != nil {
		return nil, fmt.Errorf("failed to encode image: %w", err)
	}

	return buf.Bytes(), nil
}

type ContactInfo struct {
	Name          string  `json:"name"`
	Role          string  `json:"role"`
	Phone         string  `json:"phone"`
	Email         string  `json:"email"`
	ProfilePicURL *string `json:"profilePicUrl,omitempty"`
}

type NotificationPreferencesResponse struct {
	EmailEnabled       bool `json:"emailEnabled"`
	SMSEnabled         bool `json:"smsEnabled"`
	BrowserPushEnabled bool `json:"browserPushEnabled"`
}

type UpdateNotificationPreferencesRequest struct {
	EmailEnabled       *bool `json:"emailEnabled"`
	SMSEnabled         *bool `json:"smsEnabled"`
	BrowserPushEnabled *bool `json:"browserPushEnabled"`
}

// GET /api/user/notification-preferences
func GetNotificationPreferencesHandler(w http.ResponseWriter, r *http.Request) {
	userID, ok := mw.GetUserID(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var prefs NotificationPreferencesResponse
	err := db.Conn.QueryRow(r.Context(), `
		SELECT email_enabled, sms_enabled, browser_push_enabled
		FROM user_notification_preferences
		WHERE user_id = $1
	`, userID).Scan(&prefs.EmailEnabled, &prefs.SMSEnabled, &prefs.BrowserPushEnabled)

	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			prefs = NotificationPreferencesResponse{
				EmailEnabled:       true,
				SMSEnabled:         true,
				BrowserPushEnabled: true,
			}
		} else {
			log.Printf("Failed to fetch notification preferences for user %s: %v", userID, err)
			http.Error(w, "server error", http.StatusInternalServerError)
			return
		}
	}

	_ = json.NewEncoder(w).Encode(prefs)
}

// PUT /api/user/notification-preferences
func UpdateNotificationPreferencesHandler(w http.ResponseWriter, r *http.Request) {
	userID, ok := mw.GetUserID(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req UpdateNotificationPreferencesRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid request", http.StatusBadRequest)
		return
	}

	var exists bool
	err := db.Conn.QueryRow(r.Context(), `
		SELECT EXISTS(SELECT 1 FROM user_notification_preferences WHERE user_id = $1)
	`, userID).Scan(&exists)
	if err != nil {
		log.Printf("Failed to check notification preferences for user %s: %v", userID, err)
		http.Error(w, "server error", http.StatusInternalServerError)
		return
	}

	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer func() {
		if err := tx.Rollback(r.Context()); err != nil {
			slog.Error("failed to rollback transaction", "err", err)
		}
	}()

	if exists {
		_, err = tx.Exec(r.Context(), `
			UPDATE user_notification_preferences SET
				email_enabled = COALESCE($2, email_enabled),
				sms_enabled = COALESCE($3, sms_enabled),
				browser_push_enabled = COALESCE($4, browser_push_enabled),
				updated_at = NOW()
			WHERE user_id = $1
		`, userID, req.EmailEnabled, req.SMSEnabled, req.BrowserPushEnabled)
	} else {
		_, err = tx.Exec(r.Context(), `
			INSERT INTO user_notification_preferences (user_id, email_enabled, sms_enabled, browser_push_enabled, updated_at)
			VALUES ($1, COALESCE($2, true), COALESCE($3, true), COALESCE($4, true), NOW())
		`, userID, req.EmailEnabled, req.SMSEnabled, req.BrowserPushEnabled)
	}

	if err != nil {
		log.Printf("Failed to update notification preferences for user %s: %v", userID, err)
		http.Error(w, "server error", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
}

// GET /api/contact
func GetContactInfoHandler(w http.ResponseWriter, r *http.Request) {
	var contact ContactInfo
	err := db.Conn.QueryRow(r.Context(), `
		SELECT
			COALESCE(n_first_name, '') || ' ' || COALESCE(n_last_name, '') as name,
			COALESCE(phone, ''),
			COALESCE(email, ''),
			profile_pic_url
		FROM users
		WHERE account_role = 'admin'
		ORDER BY created_at ASC
		LIMIT 1
	`).Scan(&contact.Name, &contact.Phone, &contact.Email, &contact.ProfilePicURL)

	if err != nil {
		log.Printf("Failed to get contact info: %v", err)
		http.Error(w, "contact not found", http.StatusNotFound)
		return
	}

	contact.Role = "Owner / Beauty Specialist"

	_ = json.NewEncoder(w).Encode(contact)
}
