package auth

import (
	"context"
	"crussell/auth"
	"crussell/clock"
	"crussell/db"
	"crussell/internal/dav"
	"crussell/internal/validators"
	"crussell/internal/zxcvbnjs"
	"crussell/mw"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"log/slog"
	"net/http"

	"github.com/jackc/pgx/v5"

	"os"
	"regexp"
	"strings"
	"sync"
	"time"

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

	"github.com/nyaruka/phonenumbers"
	"golang.org/x/crypto/bcrypt"
	"golang.org/x/text/cases"
	"golang.org/x/text/language"
)

const maxLoginInProgress = 20

// Login state management
var (
	loginStateMu    sync.Mutex
	loginInProgress = make(map[string]time.Time)
)

// CleanupStaleLoginEntries removes stuck loginInProgress entries older than 30 seconds.
// Called by the centralised jobs scheduler.
func CleanupStaleLoginEntries(ctx context.Context) (int, error) {
	loginStateMu.Lock()
	defer loginStateMu.Unlock()
	now := clock.Now()
	for userID, startedAt := range loginInProgress {
		if now.Sub(startedAt) > 30*time.Second {
			delete(loginInProgress, userID)
		}
	}
	return 0, nil
}

type RegisterRequest struct {
	FirstName      string `json:"firstName"    validate:"required,min=1,max=50"`
	LastName       string `json:"lastName"     validate:"required,min=1,max=50"`
	Email          string `json:"email"        validate:"required,email,max=254"`
	Password       string `json:"password"     validate:"required,min=6,max=72"`
	Phone          string `json:"phone"        validate:"required"`
	DateOfBirth    string `json:"dateOfBirth"  validate:"required"`
	AgreedToPolicy bool   `json:"agreedToPolicy"`
	ReferralCode   string `json:"referralCode,omitempty" validate:"omitempty,max=12"`
}

type LoginRequest struct {
	Email    string `json:"email"    validate:"required,email,max=254"`
	Password string `json:"password" validate:"required,max=72"`
}

// POST /api/register
func RegisterHandler(w http.ResponseWriter, r *http.Request) {
	var req RegisterRequest
	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
	}

	// Must accept terms
	if !req.AgreedToPolicy {
		http.Error(w, "must agree to terms", http.StatusBadRequest)
		return
	}

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

	// Check required fields
	if req.FirstName == "" || req.LastName == "" || req.Email == "" || req.Phone == "" || req.DateOfBirth == "" || req.Password == "" {
		http.Error(w, "all fields are required", http.StatusBadRequest)
		return
	}

	// Password must not exceed bcrypt's 72-byte limit
	if len(req.Password) > 72 {
		http.Error(w, "password must be 72 characters or less", http.StatusBadRequest)
		return
	}
	if len(req.Password) < 6 {
		http.Error(w, "password must be at least 6 characters", http.StatusBadRequest)
		return
	}
	// Server-side password strength check using the same @zxcvbn-ts/core as the frontend
	// via goja (ExecJS-style). Guarantees exact parity with frontend scoring.
	// Skipped when GO_TESTING=1 (dev/test environments) to allow weaker passwords.
	if os.Getenv("GO_TESTING") != "1" {
		passwordStrength, err := zxcvbnjs.Score(req.Password)
		if err != nil {
			log.Printf("Password strength check failed: %v", err)
			http.Error(w, "password is too weak. please choose a stronger password.", http.StatusBadRequest)
			return
		}
		if passwordStrength < 2 {
			http.Error(w, "password is too weak. please choose a stronger password.", http.StatusBadRequest)
			return
		}
	}
	// Validate name (unicode letters, spaces, hyphen, apostrophe, dot)
	nameRegex := regexp.MustCompile(`^[\p{L}\p{M}\s\-'\.]+$`)

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

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

	// Validate email format
	if err := validators.ValidateEmail(req.Email); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Normalize phone (remove spaces, hyphens, parentheses)
	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 := ValidateUKPhoneNumber(req.Phone)
	if err != nil {
		http.Error(w, "invalid phone number format", http.StatusBadRequest)
		return
	}
	req.Phone = strings.TrimSpace(phone)

	// Convert names to title case
	// Create per-call caser (cases.Caser is not goroutine-safe)
	tc := cases.Title(language.English)
	req.FirstName = tc.String(strings.ToLower(req.FirstName))
	req.LastName = tc.String(strings.ToLower(req.LastName))

	// Parse date of birth
	dob, err := time.Parse("2006-01-02", req.DateOfBirth)
	if err != nil {
		http.Error(w, "invalid date format", http.StatusBadRequest)
		return
	}

	// Reject if younger than 16
	if !dob.Before(clock.Now().AddDate(-16, 0, 0)) {
		http.Error(w, "account creation prohibited for users under 16. Please call to book an appointment.", http.StatusBadRequest)
		return
	}

	// Validate referral code if provided
	var referrerID *string
	req.ReferralCode = strings.ToLower(strings.TrimSpace(req.ReferralCode))
	if req.ReferralCode != "" {
		if len(req.ReferralCode) != 12 {
			http.Error(w, "referral code must be exactly 12 characters", http.StatusBadRequest)
			return
		}
		referralCodeRegex := regexp.MustCompile(`^[a-zA-Z0-9]{12}$`)
		if !referralCodeRegex.MatchString(req.ReferralCode) {
			http.Error(w, "referral code must be alphanumeric", http.StatusBadRequest)
			return
		}
		// Look up referrer by referral code
		err := db.Conn.QueryRow(r.Context(),
			"SELECT id FROM users WHERE referral_code = $1", req.ReferralCode).Scan(&referrerID)
		if err != nil {
			http.Error(w, "invalid referral code", http.StatusBadRequest)
			return
		}
	}

	// Hash password
	hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		http.Error(w, "server error", http.StatusInternalServerError)
		return
	}

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

	now := clock.Now()

	// Insert and return the generated ID
	var userID string
	err = tx.QueryRow(r.Context(), `
		INSERT INTO users
			(n_first_name, n_last_name, phone, date_of_birth, email, password_hash,
			 account_type, privacy_policy_and_terms_consent, policy_consent_updated_at,
			 created_at, updated_at)
		VALUES
			($1, $2, $3, $4, $5, $6, 'email', $7, $8, $8, $8)
		RETURNING id
	`, req.FirstName, req.LastName, req.Phone, dob, req.Email, string(hash), req.AgreedToPolicy, now).Scan(&userID)

	if err != nil {
		if strings.Contains(err.Error(), "duplicate key") {
			http.Error(w, "an account with this email already exists", http.StatusConflict)
		} else {
			http.Error(w, "could not create user", http.StatusInternalServerError)
		}
		return
	}

	// Record referral relationship if referral code was provided
	if referrerID != nil {
		_, err = tx.Exec(r.Context(), `
			INSERT INTO user_referrals (referrer_id, referred_id)
			VALUES ($1, $2)
			ON CONFLICT DO NOTHING
		`, *referrerID, userID)
		if err != nil {
			http.Error(w, "could not process referral", http.StatusInternalServerError)
			return
		}
	}

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

	go func() {
		defer func() {
			if r := recover(); r != nil {
				log.Printf("Panic recovered in CardDAV contact creation: %v", r)
			}
		}()
		input := dav.ContactInput{
			UserID:    userID,
			FirstName: req.FirstName,
			LastName:  req.LastName,
			Email:     req.Email,
			Phone:     req.Phone,
			DOB:       req.DateOfBirth,
		}
		if err := dav.Service.CreateContact(1, userID, input); err != nil {
			log.Printf("Warning: Failed to create contact in DAV for user %s: %v", userID, err)
		}
	}()

	w.WriteHeader(http.StatusCreated)
}

func ValidateUKPhoneNumber(phone string) (string, error) {
	num, err := phonenumbers.Parse(phone, "GB")
	if err != nil {
		return "", err
	}

	if !phonenumbers.IsValidNumber(num) {
		return "", fmt.Errorf("invalid phone number")
	}

	// Check if it's actually a UK number
	if phonenumbers.GetRegionCodeForNumber(num) != "GB" {
		return "", fmt.Errorf("only UK numbers allowed")
	}

	// Format in E.164 format (+44...)
	return phonenumbers.Format(num, phonenumbers.E164), nil
}

// POST /api/login
func LoginHandler(w http.ResponseWriter, r *http.Request) {
	var req LoginRequest
	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 email
	req.Email = strings.ToLower(strings.TrimSpace(req.Email))

	var userID, passwordHash, role string
	ctx := r.Context()
	err := db.Conn.QueryRow(ctx, `
		SELECT id, password_hash, account_role
		FROM users
		WHERE email = $1 AND account_type = 'email'
	`, req.Email).Scan(&userID, &passwordHash, &role)

	if err != nil {
		http.Error(w, "invalid credentials", http.StatusUnauthorized)
		return
	}

	// Check if account is locked
	var failedAttempts int
	var lockedUntil *time.Time
	err = db.Conn.QueryRow(r.Context(), `SELECT failed_attempts, locked_until FROM users WHERE id = $1`, userID).Scan(&failedAttempts, &lockedUntil)
	if err == nil && lockedUntil != nil && clock.Now().Before(*lockedUntil) {
		http.Error(w, "account is temporarily locked. try again later.", http.StatusTooManyRequests)
		log.Printf("LOGIN_AUDIT: locked account attempt - user=%s ip=%s", userID, middleware.GetClientIP(r.Context()))
		return
	}

	// Check if user is already logging in
	loginStateMu.Lock()
	if t, ok := loginInProgress[userID]; ok && time.Since(t) < 30*time.Second {
		loginStateMu.Unlock()
		http.Error(w, "login already in progress", http.StatusConflict) // 409
		return
	}
	// Cap the map size - drop new request if at capacity
	if len(loginInProgress) >= maxLoginInProgress {
		loginStateMu.Unlock()
		http.Error(w, "server busy, try again later", http.StatusTooManyRequests)
		return
	}
	loginInProgress[userID] = clock.Now()
	loginStateMu.Unlock()

	// Always clear flag when done
	defer func() {
		loginStateMu.Lock()
		delete(loginInProgress, userID)
		loginStateMu.Unlock()
	}()

	// Verify password
	if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
		// Increment failed attempts in DB with progressive lockout
		var newFailed int
		var newLockedUntil *time.Time
		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.QueryRow(r.Context(), `
			UPDATE users 
			SET failed_attempts = failed_attempts + 1,
			    locked_until = CASE 
				    WHEN failed_attempts + 1 >= 5 THEN NOW() + (CASE 
					    WHEN failed_attempts + 1 >= 20 THEN INTERVAL '2 hours'
					    WHEN failed_attempts + 1 >= 10 THEN INTERVAL '1 hour' 
					    WHEN failed_attempts + 1 >= 7 THEN INTERVAL '30 minutes'
					    ELSE INTERVAL '15 minutes'
				    END)
				    ELSE locked_until
			    END
			WHERE id = $1
			RETURNING failed_attempts, locked_until
		`, userID).Scan(&newFailed, &newLockedUntil)
		if err != nil {
			log.Printf("Failed to update failed login attempts: %v", err)
			http.Error(w, "Internal 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
		}

		log.Printf("LOGIN_AUDIT: failed login user=%s ip=%s attempts=%d locked_until=%v",
			userID, middleware.GetClientIP(r.Context()), newFailed, newLockedUntil)

		http.Error(w, "invalid credentials", http.StatusUnauthorized)
		return
	}

	// On success, clear lockout and update last_login
	// TODO: Password reset flow (MVP #4 in Future Work doc) must also clear
	// failed_attempts and locked_until — a locked-out user can't call this handler.
	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 failed_attempts = 0, locked_until = NULL, last_login_at = NOW() WHERE id = $1`, userID)
	if err != nil {
		log.Printf("Failed to reset login attempts on success: %v", err)
		http.Error(w, "Internal 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
	}

	// Generate JWT
	tokenString, jti, err := auth.GenerateToken(userID, role)
	if err != nil {
		http.Error(w, "could not generate token", http.StatusInternalServerError)
		return
	}

	// Generate refresh token for token rotation
	refreshToken, err := auth.GenerateRefreshToken(r.Context(), userID, role)
	if err != nil {
		log.Printf("Failed to generate refresh token: %v", err)
		http.Error(w, "could not generate refresh token", http.StatusInternalServerError)
		return
	}

	_ = json.NewEncoder(w).Encode(auth.AuthResponse{
		Token:        tokenString,
		JTI:          jti,
		RefreshToken: refreshToken,
	})
}

// POST /api/refresh-token (requires auth middleware)
func RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
	userID, _ := mw.GetUserID(r.Context())
	role, _ := mw.GetUserRole(r.Context())
	oldJTI, _ := mw.GetJTI(r.Context())

	// Verify user still exists and role hasn't changed
	var currentRole string
	err := db.Conn.QueryRow(r.Context(), `
		SELECT account_role FROM users WHERE id = $1
	`, userID).Scan(&currentRole)

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

	if currentRole != role {
		http.Error(w, "role changed, please log in again", http.StatusUnauthorized)
		return
	}

	// Revoke the old token's JTI before issuing a new one (rotation)
	if oldJTI != "" {
		auth.RevokeJTI(r.Context(), oldJTI, clock.Now().Add(90*24*time.Hour)) // match refresh token lifetime
	}

	// Generate new token
	newToken, jti, err := auth.GenerateToken(userID, currentRole)
	if err != nil {
		http.Error(w, "could not generate token", http.StatusInternalServerError)
		return
	}

	// Also issue a refresh token (opaque, stored in DB)
	refreshToken, err := auth.GenerateRefreshToken(r.Context(), userID, currentRole)
	if err != nil {
		log.Printf("Failed to generate refresh token: %v", err)
		http.Error(w, "could not generate refresh token", http.StatusInternalServerError)
		return
	}

	_ = json.NewEncoder(w).Encode(auth.AuthResponse{
		Token:        newToken,
		JTI:          jti,
		RefreshToken: refreshToken,
	})
}

// POST /api/logout (requires auth middleware)
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
	jti, ok := mw.GetJTI(r.Context())
	if !ok || jti == "" {
		http.Error(w, "invalid token", http.StatusUnauthorized)
		return
	}

	// Revoke the JTI — match the access token lifetime (1 hour)
	auth.RevokeJTI(r.Context(), jti, clock.Now().Add(1*time.Hour))

	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
}

type VerificationCodeRequest struct {
	Email string `json:"email" validate:"required,email,max=254"`
}

type VerifyCodeRequest struct {
	Code string `json:"code" validate:"required,max=64"`
}

type VerificationResponse struct {
	Success bool   `json:"success"`
	Message string `json:"message,omitempty"`
}

func GenerateVerificationCodeHandler(w http.ResponseWriter, r *http.Request) {
	var req VerificationCodeRequest
	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
	}

	email := strings.TrimSpace(strings.ToLower(req.Email))
	if email == "" {
		http.Error(w, "email is required", http.StatusBadRequest)
		return
	}

	var userID string
	err := db.Conn.QueryRow(r.Context(),
		"SELECT id FROM users WHERE LOWER(email) = $1", email,
	).Scan(&userID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			_ = json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "If the email exists, a verification code will be sent"})
			return
		}
		log.Printf("Failed to look up user: %v", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	expiresAt := clock.Now().Add(24 * time.Hour)

	var code string
	err = db.Conn.QueryRow(r.Context(),
		`INSERT INTO verification_codes (user_id, purpose, expires_at) VALUES ($1, 'email_verify', $2) RETURNING code`,
		userID, expiresAt,
	).Scan(&code)
	if err != nil {
		log.Printf("Failed to insert verification code: %v", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	_ = json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Verification code generated"})
}

func VerifyCodeHandler(w http.ResponseWriter, r *http.Request) {
	var req VerifyCodeRequest
	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
	}

	code := strings.TrimSpace(req.Code)
	if code == "" {
		http.Error(w, "code is required", http.StatusBadRequest)
		return
	}

	var userID string
	var purpose string
	var expiresAt time.Time

	err := db.Conn.QueryRow(r.Context(),
		`SELECT user_id, purpose, expires_at FROM verification_codes 
		 WHERE code = $1 AND used_at IS NULL AND expires_at > NOW()`,
		code,
	).Scan(&userID, &purpose, &expiresAt)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			// Check if code exists but was already used or expired
			var checkUsedAt *time.Time
			checkErr := db.Conn.QueryRow(r.Context(),
				`SELECT used_at FROM verification_codes WHERE code = $1`, code,
			).Scan(&checkUsedAt)
			if checkErr != nil {
				// Code doesn't exist at all
				http.Error(w, "invalid or expired code", http.StatusBadRequest)
				return
			}
			// Code exists but was already used
			if checkUsedAt != nil {
				http.Error(w, "code already used", http.StatusForbidden)
				return
			}
			// Code exists but expired
			http.Error(w, "invalid or expired code", http.StatusBadRequest)
			return
		}
		log.Printf("Failed to verify code: %v", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to start transaction: %v", err)
		http.Error(w, "internal 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 verification_codes SET used_at = NOW() WHERE code = $1`,
		code,
	)
	if err != nil {
		log.Printf("Failed to mark code as used: %v", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	if purpose == "email_verify" {
		_, err = tx.Exec(r.Context(),
			`UPDATE users SET account_role = 'verified_email' WHERE id = $1 AND account_role = 'unverified_email'`,
			userID,
		)
		if err != nil {
			log.Printf("Failed to update user role: %v", err)
			http.Error(w, "internal error", http.StatusInternalServerError)
			return
		}
	}

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

	_ = json.NewEncoder(w).Encode(VerificationResponse{Success: true, Message: "Email verified successfully"})
}


