package handlers

import (
    "crypto/hmac"
    "crypto/sha1"
    "crypto/subtle"
    "crypto/rand"
    "encoding/base32"
    "encoding/binary"
    "fmt"
    "net/http"
    "net/url"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
)

const sessionCookieName = "gobsidian_session"

// LoginPage renders the login form
func (h *Handlers) LoginPage(c *gin.Context) {
    // If already authenticated, redirect to home (respect URL prefix)
    if isAuthenticated(c) {
        c.Redirect(http.StatusFound, h.config.URLPrefix+"/")
        return
    }
    token, _ := c.Get("csrf_token")
    c.HTML(http.StatusOK, "login", gin.H{
        "app_name":        h.config.AppName,
        "csrf_token":      token,
        "ContentTemplate": "login_content",
        "ScriptsTemplate": "login_scripts",
        "Page":            "login",
    })
}

// --- Failed login tracking and automatic IP bans ---

const (
    defaultPwdFailuresThreshold = 5
    defaultMFAFailuresThreshold = 10
    defaultFailuresWindow       = 30 * time.Minute
    defaultBanDuration          = 12 * time.Hour
)

// recordFailedAttempt logs a failed attempt and applies IP ban if thresholds exceeded.
func (h *Handlers) recordFailedAttempt(c *gin.Context, typ, username string, userID *int64) {
    ip := c.ClientIP()
    var uid interface{}
    if userID != nil {
        uid = *userID
    }
    // Insert failed attempt (best-effort)
    _, _ = h.authSvc.DB.Exec(`INSERT INTO failed_logins (ip, user_id, username, type) VALUES (?,?,?,?)`, ip, uid, username, typ)

    // Determine threshold using config overrides (fallback to defaults when zero)
    threshold := h.config.PwdFailuresThreshold
    if threshold <= 0 {
        threshold = defaultPwdFailuresThreshold
    }
    if typ == "mfa" {
        t2 := h.config.MFAFailuresThreshold
        if t2 <= 0 {
            t2 = defaultMFAFailuresThreshold
        }
        threshold = t2
    }
    // Count recent failures for this IP and type
    var cnt int
    // Window from config (minutes)
    windowMinutes := h.config.FailuresWindowMinutes
    if windowMinutes <= 0 {
        windowMinutes = int(defaultFailuresWindow / time.Minute)
    }
    cutoff := time.Now().Add(-time.Duration(windowMinutes) * time.Minute)
    _ = h.authSvc.DB.QueryRow(`SELECT COUNT(1) FROM failed_logins WHERE ip = ? AND type = ? AND created_at >= ?`, ip, typ, cutoff).Scan(&cnt)
    if cnt >= threshold {
        // Duration/permanence from config
        banHours := h.config.AutoBanDurationHours
        if banHours <= 0 {
            banHours = int(defaultBanDuration / time.Hour)
        }
        makePermanent := h.config.AutoBanPermanent
        var until interface{}
        if makePermanent {
            until = nil
        } else {
            until = time.Now().Add(time.Duration(banHours) * time.Hour)
        }
        reason := fmt.Sprintf("auto-ban: %s failures=%d within %d minutes", typ, cnt, windowMinutes)
        // Upsert ban unless whitelisted or permanent existing
        if makePermanent {
            _, _ = h.authSvc.DB.Exec(`
                INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, created_at, updated_at)
                VALUES (?, ?, NULL, 1, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                ON CONFLICT(ip) DO UPDATE SET
                    reason=excluded.reason,
                    until=NULL,
                    permanent=1,
                    updated_at=CURRENT_TIMESTAMP
                WHERE ip_bans.whitelisted = 0
            `, ip, reason)
        } else {
            _, _ = h.authSvc.DB.Exec(`
                INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, created_at, updated_at)
                VALUES (?, ?, ?, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
                ON CONFLICT(ip) DO UPDATE SET
                    reason=excluded.reason,
                    until=excluded.until,
                    permanent=0,
                    updated_at=CURRENT_TIMESTAMP
                WHERE ip_bans.whitelisted = 0 AND ip_bans.permanent = 0
            `, ip, reason, until)
        }
    }
}

// isAllDigits returns true if s consists only of ASCII digits 0-9
func isAllDigits(s string) bool {
    if s == "" {
        return false
    }
    for i := 0; i < len(s); i++ {
        if s[i] < '0' || s[i] > '9' {
            return false
        }
    }
    return true
}

// MFALoginPage shows OTP prompt when MFA is enabled
func (h *Handlers) MFALoginPage(c *gin.Context) {
    session, _ := h.store.Get(c.Request, sessionCookieName)
    if _, ok := session.Values["mfa_user_id"]; !ok {
        c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
        return
    }
    token, _ := c.Get("csrf_token")
    c.HTML(http.StatusOK, "mfa", gin.H{
        "app_name":        h.config.AppName,
        "csrf_token":      token,
        "ContentTemplate": "mfa_content",
        "ScriptsTemplate": "mfa_scripts",
        "Page":            "mfa",
    })
}

// MFALoginVerify verifies OTP and completes login
func (h *Handlers) MFALoginVerify(c *gin.Context) {
    code := strings.TrimSpace(c.PostForm("code"))
    if len(code) != 6 || !isAllDigits(code) {
        token, _ := c.Get("csrf_token")
        // record failed MFA attempt
        h.recordFailedAttempt(c, "mfa", "", nil)
        c.HTML(http.StatusUnauthorized, "mfa", gin.H{
            "app_name":        h.config.AppName,
            "csrf_token":      token,
            "error":           "Invalid code format",
            "ContentTemplate": "mfa_content",
            "ScriptsTemplate": "mfa_scripts",
            "Page":            "mfa",
        })
        return
    }
    session, _ := h.store.Get(c.Request, sessionCookieName)
    uidAny, ok := session.Values["mfa_user_id"]
    if !ok {
        c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
        return
    }
    uid, _ := uidAny.(int64)
    var secret string
    if err := h.authSvc.DB.QueryRow(`SELECT mfa_secret FROM users WHERE id = ?`, uid).Scan(&secret); err != nil || secret == "" {
        h.recordFailedAttempt(c, "mfa", "", &uid)
        c.HTML(http.StatusUnauthorized, "mfa", gin.H{"error": "MFA not enabled", "Page": "mfa", "ContentTemplate": "mfa_content", "ScriptsTemplate": "mfa_scripts", "app_name": h.config.AppName})
        return
    }
    if !verifyTOTP(secret, code, time.Now()) {
        token, _ := c.Get("csrf_token")
        h.recordFailedAttempt(c, "mfa", "", &uid)
        c.HTML(http.StatusUnauthorized, "mfa", gin.H{
            "app_name":        h.config.AppName,
            "csrf_token":      token,
            "error":           "Invalid code",
            "ContentTemplate": "mfa_content",
            "ScriptsTemplate": "mfa_scripts",
            "Page":            "mfa",
        })
        return
    }
    // success: set user_id and clear mfa_user_id
    delete(session.Values, "mfa_user_id")
    session.Values["user_id"] = uid
    _ = session.Save(c.Request, c.Writer)
    c.Redirect(http.StatusFound, h.config.URLPrefix+"/")
}

// ProfileMFASetupPage shows QR and input to verify during enrollment
func (h *Handlers) ProfileMFASetupPage(c *gin.Context) {
    uidPtr := getUserIDPtr(c)
    if uidPtr == nil {
        c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
        return
    }
    // ensure enrollment exists, otherwise create one
    var secret string
    err := h.authSvc.DB.QueryRow(`SELECT secret FROM mfa_enrollments WHERE user_id = ?`, *uidPtr).Scan(&secret)
    if err != nil || secret == "" {
        // create new enrollment
        s, e := generateBase32Secret()
        if e != nil {
            c.HTML(http.StatusInternalServerError, "error", gin.H{"error": "Failed to create enrollment", "Page": "error", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "app_name": h.config.AppName})
            return
        }
        _, _ = h.authSvc.DB.Exec(`INSERT OR REPLACE INTO mfa_enrollments (user_id, secret) VALUES (?, ?)`, *uidPtr, s)
        secret = s
    }
    // Fetch username for label
    var username string
    _ = h.authSvc.DB.QueryRow(`SELECT username FROM users WHERE id = ?`, *uidPtr).Scan(&username)
    issuer := h.config.AppName
    label := url.PathEscape(fmt.Sprintf("%s:%s", issuer, username))
    otpauth := fmt.Sprintf("otpauth://totp/%s?secret=%s&issuer=%s&digits=6&period=30&algorithm=SHA1", label, secret, url.QueryEscape(issuer))

    // Render simple page (uses base.html shell)
    c.HTML(http.StatusOK, "mfa_setup", gin.H{
        "app_name":        h.config.AppName,
        "Secret":          secret,
        "OTPAuthURI":      otpauth,
        "ContentTemplate": "mfa_setup_content",
        "ScriptsTemplate": "mfa_setup_scripts",
        "Page":            "mfa_setup",
    })
}

// ProfileMFASetupVerify finalizes enrollment by verifying a TOTP code and setting mfa_secret
func (h *Handlers) ProfileMFASetupVerify(c *gin.Context) {
    uidPtr := getUserIDPtr(c)
    if uidPtr == nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
        return
    }
    code := strings.TrimSpace(c.PostForm("code"))
    if len(code) != 6 || !isAllDigits(code) {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid code format"})
        return
    }
    var secret string
    if err := h.authSvc.DB.QueryRow(`SELECT secret FROM mfa_enrollments WHERE user_id = ?`, *uidPtr).Scan(&secret); err != nil || secret == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "no enrollment found"})
        return
    }
    if !verifyTOTP(secret, code, time.Now()) {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid code"})
        return
    }
    // move secret to users and delete enrollment
    if _, err := h.authSvc.DB.Exec(`UPDATE users SET mfa_secret = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, secret, *uidPtr); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    _, _ = h.authSvc.DB.Exec(`DELETE FROM mfa_enrollments WHERE user_id = ?`, *uidPtr)
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// TOTP helpers (SHA1, 30s window, 6 digits)
func generateBase32Secret() (string, error) {
    // 20 random bytes -> base32 without padding
    b := make([]byte, 20)
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    enc := base32.StdEncoding.WithPadding(base32.NoPadding)
    return enc.EncodeToString(b), nil
}

func hotp(secret []byte, counter uint64) string {
    // HMAC-SHA1
    var buf [8]byte
    binary.BigEndian.PutUint64(buf[:], counter)
    mac := hmac.New(sha1.New, secret)
    mac.Write(buf[:])
    sum := mac.Sum(nil)
    // dynamic truncation
    offset := sum[len(sum)-1] & 0x0F
    code := (uint32(sum[offset])&0x7F)<<24 | (uint32(sum[offset+1])&0xFF)<<16 | (uint32(sum[offset+2])&0xFF)<<8 | (uint32(sum[offset+3]) & 0xFF)
    return fmt.Sprintf("%06d", code%1000000)
}

func verifyTOTP(base32Secret, code string, t time.Time) bool {
    enc := base32.StdEncoding.WithPadding(base32.NoPadding)
    key, err := enc.DecodeString(strings.ToUpper(base32Secret))
    if err != nil {
        return false
    }
    timestep := uint64(t.Unix() / 30)
    // allow +/- 1 window
    candidates := []string{
        hotp(key, timestep-1),
        hotp(key, timestep),
        hotp(key, timestep+1),
    }
    for _, c := range candidates {
        if subtle.ConstantTimeCompare([]byte(c), []byte(code)) == 1 {
            return true
        }
    }
    return false
}

// LoginPost processes the login form
func (h *Handlers) LoginPost(c *gin.Context) {
    username := c.PostForm("username")
    password := c.PostForm("password")

    user, err := h.authSvc.Authenticate(username, password)
    if err != nil {
        token, _ := c.Get("csrf_token")
        // record failed password attempt
        h.recordFailedAttempt(c, "password", username, nil)
        c.HTML(http.StatusUnauthorized, "login", gin.H{
            "app_name":        h.config.AppName,
            "csrf_token":      token,
            "error":           err.Error(),
            "ContentTemplate": "login_content",
            "ScriptsTemplate": "login_scripts",
            "Page":            "login",
        })
        return
    }
    // If user has MFA enabled, require OTP before setting user_id
    if user.MFASecret.Valid && user.MFASecret.String != "" {
        session, _ := h.store.Get(c.Request, sessionCookieName)
        session.Values["mfa_user_id"] = user.ID
        _ = session.Save(c.Request, c.Writer)
        c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/mfa")
        return
    }

    // If admin created an enrollment for this user, force MFA setup after login
    var pending int
    if err := h.authSvc.DB.QueryRow(`SELECT 1 FROM mfa_enrollments WHERE user_id = ?`, user.ID).Scan(&pending); err == nil {
        // normal login, then redirect to setup
        session, _ := h.store.Get(c.Request, sessionCookieName)
        session.Values["user_id"] = user.ID
        _ = session.Save(c.Request, c.Writer)
        c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/profile/mfa/setup")
        return
    }

    // Create normal session
    session, _ := h.store.Get(c.Request, sessionCookieName)
    session.Values["user_id"] = user.ID
    _ = session.Save(c.Request, c.Writer)

    c.Redirect(http.StatusFound, h.config.URLPrefix+"/")
}

// LogoutPost clears the session
func (h *Handlers) LogoutPost(c *gin.Context) {
    session, _ := h.store.Get(c.Request, sessionCookieName)
    session.Options.MaxAge = -1
    _ = session.Save(c.Request, c.Writer)
    c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
}
