package webui

import (
	"net/http"
	"strconv"
	"strings"

	"github.com/pquerna/otp/totp"
	"mailgoserver/internal/db"
)

// pendingMFACookie holds the user id awaiting a second factor, set right after a
// successful password check and cleared once MFA passes (or the user logs out).
// Kept separate from the real session cookie so an unfinished login never grants
// access to anything.
const pendingMFACookieName = "mailgoserver_pending_mfa"

func setPendingMFACookie(w http.ResponseWriter, userID string) {
	http.SetCookie(w, &http.Cookie{
		Name: pendingMFACookieName, Value: userID, Path: "/", HttpOnly: true,
		SameSite: http.SameSiteLaxMode, MaxAge: 10 * 60,
	})
}
func clearPendingMFACookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{Name: pendingMFACookieName, Value: "", Path: "/", MaxAge: -1})
}
func pendingMFAUserID(r *http.Request) int64 {
	c, err := r.Cookie(pendingMFACookieName)
	if err != nil {
		return 0
	}
	return int64(atoi(c.Value))
}

func (a *App) loginForm(w http.ResponseWriter, r *http.Request) {
	if sess, user, _ := a.currentSession(r); sess != nil && user != nil {
		http.Redirect(w, r, Prefix+"/", http.StatusFound)
		return
	}
	a.render(w, r, "login.html", M{"next": r.URL.Query().Get("next")})
}

// loginSubmit checks username+password, then either starts a fully-verified session
// (no second factor enabled) or a pending-MFA state that requires /login/mfa next.
func (a *App) loginSubmit(w http.ResponseWriter, r *http.Request) {
	if !a.rateLimitLogin(w, r) {
		return
	}
	username := strings.TrimSpace(r.FormValue("username"))
	password := r.FormValue("password")
	next := r.FormValue("next")

	fail := func(msg string) {
		a.render(w, r, "login.html", M{"error": msg, "username": username, "next": next})
	}

	if a.accountLocked("admin_login", username) {
		fail("Too many failed attempts for this account. Try again later.")
		return
	}

	user, err := a.DB.GetAdminUserByUsername(username)
	if err != nil {
		a.Logger.Error("login lookup: %v", err)
		fail("Something went wrong. Try again.")
		return
	}
	if user == nil || !db.CheckPassword(password, user.PasswordHash) {
		_ = a.DB.LogAuthAttempt("admin_login", username, a.requestIP(r), false, "Incorrect username or password")
		fail("Incorrect username or password.")
		return
	}

	needsMFA := user.TOTPEnabled
	if !needsMFA {
		if n, _ := a.DB.CountWebAuthnCredentials(user.ID); n > 0 {
			needsMFA = true
		}
	}

	if !needsMFA {
		token, err := a.DB.CreateSession(user.ID, true, sessionTTL)
		if err != nil {
			fail("Something went wrong. Try again.")
			return
		}
		_ = a.DB.LogAuthAttempt("admin_login", username, a.requestIP(r), true, "Login successful")
		setSessionCookie(w, token, r.TLS != nil)
		http.Redirect(w, r, redirectTarget(next), http.StatusFound)
		return
	}

	setPendingMFACookie(w, strconv.FormatInt(user.ID, 10))
	http.Redirect(w, r, Prefix+"/login/mfa?next="+next, http.StatusFound)
}

func redirectTarget(next string) string {
	if next == "" || !strings.HasPrefix(next, Prefix) {
		return Prefix + "/"
	}
	return next
}

func (a *App) mfaForm(w http.ResponseWriter, r *http.Request) {
	userID := pendingMFAUserID(r)
	if userID == 0 {
		http.Redirect(w, r, Prefix+"/login", http.StatusFound)
		return
	}
	user, _ := a.DB.GetAdminUserByID(userID)
	if user == nil {
		clearPendingMFACookie(w)
		http.Redirect(w, r, Prefix+"/login", http.StatusFound)
		return
	}
	hasPasskeys, _ := a.DB.CountWebAuthnCredentials(userID)
	a.render(w, r, "login_mfa.html", M{
		"next": r.URL.Query().Get("next"), "totp_enabled": user.TOTPEnabled, "has_passkeys": hasPasskeys > 0,
	})
}

// mfaSubmit verifies the TOTP code for the pending login and, on success, promotes
// the pending state into a real, fully-verified session.
func (a *App) mfaSubmit(w http.ResponseWriter, r *http.Request) {
	if !a.rateLimitLogin(w, r) {
		return
	}
	userID := pendingMFAUserID(r)
	next := r.FormValue("next")
	if userID == 0 {
		http.Redirect(w, r, Prefix+"/login", http.StatusFound)
		return
	}
	user, err := a.DB.GetAdminUserByID(userID)
	if err != nil || user == nil {
		clearPendingMFACookie(w)
		http.Redirect(w, r, Prefix+"/login", http.StatusFound)
		return
	}

	code := strings.TrimSpace(r.FormValue("code"))
	if !user.TOTPEnabled || !totp.Validate(code, user.TOTPSecret) {
		_ = a.DB.LogAuthAttempt("admin_login", user.Username, a.requestIP(r), false, "Invalid MFA code")
		hasPasskeys, _ := a.DB.CountWebAuthnCredentials(userID)
		a.render(w, r, "login_mfa.html", M{
			"next": next, "totp_enabled": user.TOTPEnabled, "has_passkeys": hasPasskeys > 0, "error": "Invalid code.",
		})
		return
	}

	token, err := a.DB.CreateSession(user.ID, true, sessionTTL)
	if err != nil {
		a.Logger.Error("create session: %v", err)
		http.Redirect(w, r, Prefix+"/login", http.StatusFound)
		return
	}
	_ = a.DB.LogAuthAttempt("admin_login", user.Username, a.requestIP(r), true, "Login successful (authenticator app)")
	clearPendingMFACookie(w)
	setSessionCookie(w, token, r.TLS != nil)
	http.Redirect(w, r, redirectTarget(next), http.StatusFound)
}

func (a *App) logout(w http.ResponseWriter, r *http.Request) {
	if c, err := r.Cookie(sessionCookieName); err == nil {
		_ = a.DB.DeleteSession(c.Value)
	}
	clearSessionCookie(w)
	http.Redirect(w, r, Prefix+"/login", http.StatusFound)
}

func (a *App) firstLoginForm(w http.ResponseWriter, r *http.Request) {
	user := userFromContext(r)
	a.render(w, r, "first_login.html", M{"username": user.Username, "must_change_username": user.MustChangeUsername})
}

// firstLoginSubmit mirrors the forced credential-change flow: always require a new
// password before must_change_password clears; only the seeded default admin
// (MustChangeUsername) is additionally required to pick a new username — a delegated
// admin already chose their own username when the account was created, so re-asking
// for one here would just be busywork with no security purpose.
func (a *App) firstLoginSubmit(w http.ResponseWriter, r *http.Request) {
	user := userFromContext(r)
	newPassword := r.FormValue("password")
	confirm := r.FormValue("password_confirm")

	fail := func(msg string) {
		a.render(w, r, "first_login.html", M{
			"username": r.FormValue("username"), "must_change_username": user.MustChangeUsername, "error": msg,
		})
	}

	if !isStrongPassword(newPassword) {
		fail("Password must be at least 10 characters and include a letter, a number, and a symbol.")
		return
	}
	if newPassword != confirm {
		fail("Passwords don't match.")
		return
	}

	hash, err := db.HashPassword(newPassword)
	if err != nil {
		fail("Something went wrong. Try again.")
		return
	}

	if !user.MustChangeUsername {
		if err := a.DB.UpdateAdminPasswordClearMustChange(user.ID, hash); err != nil {
			fail("Something went wrong. Try again.")
			return
		}
		setFlash(w, "success", "Password updated. Welcome to your dashboard.")
		http.Redirect(w, r, Prefix+"/", http.StatusFound)
		return
	}

	newUsername := strings.TrimSpace(r.FormValue("username"))
	if newUsername == "" {
		fail("Choose a username.")
		return
	}
	if existing, _ := a.DB.GetAdminUserByUsername(newUsername); existing != nil && existing.ID != user.ID {
		fail("That username is already taken.")
		return
	}
	if err := a.DB.UpdateAdminCredentials(user.ID, newUsername, hash); err != nil {
		fail("Something went wrong. Try again.")
		return
	}
	setFlash(w, "success", "Credentials updated. Welcome to your dashboard.")
	http.Redirect(w, r, Prefix+"/", http.StatusFound)
}

// isStrongPassword requires the same practical minimum most providers enforce: not
// the literal default, long enough, and not just letters.
func isStrongPassword(pw string) bool {
	if len(pw) < 10 {
		return false
	}
	var hasLetter, hasDigit, hasSymbol bool
	for _, c := range pw {
		switch {
		case c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z':
			hasLetter = true
		case c >= '0' && c <= '9':
			hasDigit = true
		default:
			hasSymbol = true
		}
	}
	return hasLetter && hasDigit && hasSymbol
}
