package handlers

import (
	"database/sql"
	"encoding/base64"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"crypto/rand"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/sessions"
	"github.com/h2non/filetype"
	"golang.org/x/crypto/bcrypt"

	"gobsidian/internal/config"
	"gobsidian/internal/auth"
	"gobsidian/internal/markdown"
	"gobsidian/internal/models"
	"gobsidian/internal/utils"
)

type Handlers struct {
	config   *config.Config
	store    *sessions.CookieStore
	renderer *markdown.Renderer
	authSvc  *auth.Service
}

// AdminBanIP bans an IP (temporary via hours or permanent)
func (h *Handlers) AdminBanIP(c *gin.Context) {
    ip := strings.TrimSpace(c.PostForm("ip"))
    if ip == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ip is required"})
        return
    }
    reason := strings.TrimSpace(c.PostForm("reason"))
    permStr := strings.TrimSpace(c.PostForm("permanent"))
    hoursStr := strings.TrimSpace(c.PostForm("hours"))
    permanent := 0
    if permStr == "1" || strings.EqualFold(permStr, "true") {
        permanent = 1
    }

    var until sql.NullTime
    if permanent == 0 && hoursStr != "" {
        if hInt, err := strconv.Atoi(hoursStr); err == nil && hInt > 0 {
            t := time.Now().Add(time.Duration(hInt) * time.Hour)
            until = sql.NullTime{Time: t, Valid: true}
        }
    }

    // Upsert ban
    if permanent == 1 {
        _, err := h.authSvc.DB.Exec(`INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, updated_at)
            VALUES (?, ?, NULL, 1, 0, CURRENT_TIMESTAMP)
            ON CONFLICT(ip) DO UPDATE SET reason=excluded.reason, until=NULL, permanent=1, whitelisted=0, updated_at=CURRENT_TIMESTAMP`, ip, nullIfEmpty(reason))
        if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}); return }
    } else {
        _, err := h.authSvc.DB.Exec(`INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, updated_at)
            VALUES (?, ?, ?, 0, 0, CURRENT_TIMESTAMP)
            ON CONFLICT(ip) DO UPDATE SET reason=excluded.reason, until=excluded.until, permanent=0, whitelisted=0, updated_at=CURRENT_TIMESTAMP`, ip, nullIfEmpty(reason), until)
        if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}); return }
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminUnbanIP removes a ban entry (and whitelist) for an IP
func (h *Handlers) AdminUnbanIP(c *gin.Context) {
    ip := strings.TrimSpace(c.PostForm("ip"))
    if ip == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ip is required"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`DELETE FROM ip_bans WHERE ip = ?`, ip); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminWhitelistIP sets or clears whitelist for an IP
func (h *Handlers) AdminWhitelistIP(c *gin.Context) {
    ip := strings.TrimSpace(c.PostForm("ip"))
    if ip == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ip is required"})
        return
    }
    val := strings.TrimSpace(c.PostForm("value"))
    w := 0
    if val == "1" || strings.EqualFold(val, "true") { w = 1 }
    // Ensure row exists and update flags
    _, err := h.authSvc.DB.Exec(`INSERT INTO ip_bans (ip, reason, until, permanent, whitelisted, updated_at)
        VALUES (?, NULL, NULL, 0, ?, CURRENT_TIMESTAMP)
        ON CONFLICT(ip) DO UPDATE SET whitelisted=excluded.whitelisted, permanent=CASE WHEN excluded.whitelisted=1 THEN 0 ELSE permanent END, until=CASE WHEN excluded.whitelisted=1 THEN NULL ELSE until END, updated_at=CURRENT_TIMESTAMP`, ip, w)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}


// helper: turn empty string to sql.NullString
func nullIfEmpty(s string) sql.NullString {
    if strings.TrimSpace(s) == "" { return sql.NullString{Valid: false} }
    return sql.NullString{String: s, Valid: true}
}

// AdminLogsPage shows recent access, error, failed login, and IP ban entries
func (h *Handlers) AdminLogsPage(c *gin.Context) {

    // Parse filters
    ipFilter := strings.TrimSpace(c.Query("ip"))
    hoursStr := strings.TrimSpace(c.Query("hours"))
    limitStr := strings.TrimSpace(c.Query("limit"))
    bansFilter := strings.TrimSpace(c.Query("bans")) // all|active|perma|whitelist

    // Defaults
    var lastSince *time.Time
    if hoursStr != "" {
        if hInt, err := strconv.Atoi(hoursStr); err == nil && hInt > 0 {
            t := time.Now().Add(-time.Duration(hInt) * time.Hour)
            lastSince = &t
        }
    }
    lim := 100
    if limitStr != "" {
        if v, err := strconv.Atoi(limitStr); err == nil {
            if v <= 0 {
                lim = 100
            } else if v > 500 {
                lim = 500
            } else {
                lim = v
            }
        }
    }
    if bansFilter == "" {
        bansFilter = "all"
    }

    // Query recent entries with filters
    type accessRow struct {
        CreatedAt time.Time
        IP        string
        Method    string
        Path      string
        Status    int
        Duration  int64
        UserAgent string
        UserID    sql.NullInt64
        Username  sql.NullString
    }
    type errorRow struct {
        CreatedAt time.Time
        IP        sql.NullString
        Path      sql.NullString
        Message   string
    }
    type failedRow struct {
        CreatedAt time.Time
        IP        string
        Username  sql.NullString
        Type      string
        UserID    sql.NullInt64
    }
    type banRow struct {
        IP          string
        Reason      sql.NullString
        Until       sql.NullTime
        Permanent   int
        Whitelisted int
        UpdatedAt   time.Time
    }

    // Access logs
    access := []accessRow{}
    {
        sb := strings.Builder{}
        sb.WriteString("SELECT al.created_at, al.ip, al.method, al.path, al.status, al.duration_ms, al.user_agent, al.user_id, u.username FROM access_logs al LEFT JOIN users u ON u.id = al.user_id WHERE 1=1")
        args := []any{}
        if lastSince != nil {
            sb.WriteString(" AND al.created_at >= ?")
            args = append(args, *lastSince)
        }
        if ipFilter != "" {
            sb.WriteString(" AND al.ip LIKE ?")
            args = append(args, "%"+ipFilter+"%")
        }
        sb.WriteString(" ORDER BY al.created_at DESC LIMIT ")
        sb.WriteString(strconv.Itoa(lim))
        if rows, err := h.authSvc.DB.Query(sb.String(), args...); err == nil {
            defer rows.Close()
            for rows.Next() {
                var r accessRow
                if err := rows.Scan(&r.CreatedAt, &r.IP, &r.Method, &r.Path, &r.Status, &r.Duration, &r.UserAgent, &r.UserID, &r.Username); err == nil {
                    access = append(access, r)
                }
            }
        }
    }

    // Error logs
    errors := []errorRow{}
    {
        sb := strings.Builder{}
        sb.WriteString("SELECT created_at, ip, path, message FROM error_logs WHERE 1=1")
        args := []any{}
        if lastSince != nil {
            sb.WriteString(" AND created_at >= ?")
            args = append(args, *lastSince)
        }
        if ipFilter != "" {
            sb.WriteString(" AND ip IS NOT NULL AND ip LIKE ?")
            args = append(args, "%"+ipFilter+"%")
        }
        sb.WriteString(" ORDER BY created_at DESC LIMIT ")
        sb.WriteString(strconv.Itoa(lim))
        if rows, err := h.authSvc.DB.Query(sb.String(), args...); err == nil {
            defer rows.Close()
            for rows.Next() {
                var r errorRow
                if err := rows.Scan(&r.CreatedAt, &r.IP, &r.Path, &r.Message); err == nil {
                    errors = append(errors, r)
                }
            }
        }
    }

    // Failed logins
    failed := []failedRow{}
    {
        sb := strings.Builder{}
        sb.WriteString("SELECT created_at, ip, username, type, user_id FROM failed_logins WHERE 1=1")
        args := []any{}
        if lastSince != nil {
            sb.WriteString(" AND created_at >= ?")
            args = append(args, *lastSince)
        }
        if ipFilter != "" {
            sb.WriteString(" AND ip LIKE ?")
            args = append(args, "%"+ipFilter+"%")
        }
        sb.WriteString(" ORDER BY created_at DESC LIMIT ")
        sb.WriteString(strconv.Itoa(lim))
        if rows, err := h.authSvc.DB.Query(sb.String(), args...); err == nil {
            defer rows.Close()
            for rows.Next() {
                var r failedRow
                if err := rows.Scan(&r.CreatedAt, &r.IP, &r.Username, &r.Type, &r.UserID); err == nil {
                    failed = append(failed, r)
                }
            }
        }
    }

    // IP bans
    bans := []banRow{}
    {
        sb := strings.Builder{}
        sb.WriteString("SELECT ip, reason, until, permanent, whitelisted, updated_at FROM ip_bans WHERE 1=1")
        args := []any{}
        switch strings.ToLower(bansFilter) {
        case "perma", "perm", "permanent":
            sb.WriteString(" AND permanent = 1")
        case "whitelist", "whitelisted":
            sb.WriteString(" AND whitelisted = 1")
        case "active":
            // Active means currently in effect and not whitelisted
            sb.WriteString(" AND whitelisted = 0 AND (permanent = 1 OR (until IS NOT NULL AND until > CURRENT_TIMESTAMP))")
        default:
            // all
        }
        if ipFilter != "" {
            sb.WriteString(" AND ip LIKE ?")
            args = append(args, "%"+ipFilter+"%")
        }
        sb.WriteString(" ORDER BY updated_at DESC LIMIT ")
        sb.WriteString(strconv.Itoa(lim))
        if rows, err := h.authSvc.DB.Query(sb.String(), args...); err == nil {
            defer rows.Close()
            for rows.Next() {
                var r banRow
                if err := rows.Scan(&r.IP, &r.Reason, &r.Until, &r.Permanent, &r.Whitelisted, &r.UpdatedAt); err == nil {
                    bans = append(bans, r)
                }
            }
        }
    }

    c.HTML(http.StatusOK, "admin_logs", gin.H{
        "app_name":        h.config.AppName,
        "NoSidebar":       true,
        "breadcrumbs":     []gin.H{{"Name": "/", "URL": "/"}, {"Name": "Admin", "URL": "/editor/admin"}, {"Name": "Logs", "URL": ""}},
        "Page":            "admin_logs",
        "AccessLogs":      access,
        "ErrorLogs":       errors,
        "FailedLogins":    failed,
        "IPBans":          bans,
        // Echo filters back to template
        "FilterIP":        ipFilter,
        "FilterHours":     hoursStr,
        "FilterLimit":     lim,
        "FilterBans":      bansFilter,
    })
}

// AdminClearAccessLogs clears access logs based on days parameter: 0=all, default 7
func (h *Handlers) AdminClearAccessLogs(c *gin.Context) {
    daysStr := strings.TrimSpace(c.PostForm("days"))
    days := 7
    if daysStr != "" {
        if v, err := strconv.Atoi(daysStr); err == nil && v >= 0 {
            days = v
        }
    }
    var (
        query string
        args  []any
    )
    if days == 0 {
        query = `DELETE FROM access_logs`
    } else {
        query = `DELETE FROM access_logs WHERE created_at < DATETIME('now', ? || ' days')`
        // negative days for SQLite modifier, e.g., '-7 days'
        args = append(args, fmt.Sprintf("-%d", days))
    }
    if _, err := h.authSvc.DB.Exec(query, args...); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// ProfilePage renders the user profile page for the signed-in user
func (h *Handlers) ProfilePage(c *gin.Context) {
    // Must be authenticated; middleware ensures user_id is set
    uidPtr := getUserIDPtr(c)
    if uidPtr == nil {
        c.Redirect(http.StatusFound, h.config.URLPrefix+"/editor/login")
        return
    }

    // Load notes tree for sidebar
    notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config)
    if err != nil {
        c.HTML(http.StatusInternalServerError, "error", gin.H{
            "error":            "Failed to build tree structure",
            "app_name":         h.config.AppName,
            "message":          err.Error(),
            "ContentTemplate":  "error_content",
            "ScriptsTemplate":  "error_scripts",
            "Page":             "error",
        })
        return
    }

    // Fetch current user basic info
    var email string
    var mfa sql.NullString
    row := h.authSvc.DB.QueryRow(`SELECT email, mfa_secret FROM users WHERE id = ?`, *uidPtr)
    if err := row.Scan(&email, &mfa); err != nil {
        c.HTML(http.StatusInternalServerError, "error", gin.H{
            "error":            "Failed to load profile",
            "app_name":         h.config.AppName,
            "message":          err.Error(),
            "ContentTemplate":  "error_content",
            "ScriptsTemplate":  "error_scripts",
            "Page":             "error",
        })
        return
    }

    c.HTML(http.StatusOK, "profile", gin.H{
        "app_name":        h.config.AppName,
        "notes_tree":      notesTree,
        "active_path":     []string{},
        "current_note":    nil,
        "breadcrumbs":     utils.GenerateBreadcrumbs(""),
        "Authenticated":   true,
        "IsAdmin":         isAdmin(c),
        "Email":           email,
        "MFAEnabled":      mfa.Valid && mfa.String != "",
        "ContentTemplate": "profile_content",
        "ScriptsTemplate": "profile_scripts",
        "Page":            "profile",
    })
}

// PostProfileChangePassword allows the user to change their password with current password verification
func (h *Handlers) PostProfileChangePassword(c *gin.Context) {
    uidPtr := getUserIDPtr(c)
    if uidPtr == nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
        return
    }

    current := c.PostForm("current_password")
    newpw := c.PostForm("new_password")
    confirm := c.PostForm("confirm_password")
    if current == "" || newpw == "" || confirm == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "all password fields are required"})
        return
    }
    if newpw != confirm {
        c.JSON(http.StatusBadRequest, gin.H{"error": "new password and confirmation do not match"})
        return
    }
    if len(newpw) < 8 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "password must be at least 8 characters"})
        return
    }

    var pwHash string
    row := h.authSvc.DB.QueryRow(`SELECT password_hash FROM users WHERE id = ?`, *uidPtr)
    if err := row.Scan(&pwHash); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"})
        return
    }
    if err := bcrypt.CompareHashAndPassword([]byte(pwHash), []byte(current)); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "current password is incorrect"})
        return
    }
    // Hash new password
    newHashBytes, err := bcrypt.GenerateFromPassword([]byte(newpw), bcrypt.DefaultCost)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, string(newHashBytes), *uidPtr); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// PostProfileChangeEmail allows the user to update their email
func (h *Handlers) PostProfileChangeEmail(c *gin.Context) {
    uidPtr := getUserIDPtr(c)
    if uidPtr == nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
        return
    }
    email := strings.TrimSpace(c.PostForm("email"))
    if email == "" || !strings.Contains(email, "@") {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid email"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`UPDATE users SET email = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, email, *uidPtr); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// PostProfileEnableMFA generates and stores a new MFA secret for the user
func (h *Handlers) PostProfileEnableMFA(c *gin.Context) {
    uidPtr := getUserIDPtr(c)
    if uidPtr == nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
        return
    }
    // Create or replace enrollment for this user
    secret, err := generateBase32Secret()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate secret"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`INSERT OR REPLACE INTO mfa_enrollments (user_id, secret) VALUES (?, ?)`, *uidPtr, secret); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true, "setup": true, "redirect": h.config.URLPrefix+"/editor/profile/mfa/setup"})
}

// PostProfileDisableMFA clears the user's MFA secret
func (h *Handlers) PostProfileDisableMFA(c *gin.Context) {
    uidPtr := getUserIDPtr(c)
    if uidPtr == nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`UPDATE users SET mfa_secret = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, *uidPtr); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminCreateUser creates a new user (admin only)
func (h *Handlers) AdminCreateUser(c *gin.Context) {
    username := strings.TrimSpace(c.PostForm("username"))
    email := strings.TrimSpace(c.PostForm("email"))
    password := c.PostForm("password")
    if username == "" || email == "" || password == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "username, email and password are required"})
        return
    }
    // hash password
    pwHashBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`INSERT INTO users (username, email, password_hash, is_active, email_confirmed) VALUES (?,?,?,?,?)`,
        username, email, string(pwHashBytes), 1, 1,
    ); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminDeleteUser deletes a user by id (admin only)
func (h *Handlers) AdminDeleteUser(c *gin.Context) {
    idStr := c.Param("id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil || id <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
        return
    }
    // prevent deleting own account
    if v, ok := c.Get("user_id"); ok {
        if uid, ok2 := v.(int64); ok2 && uid == id {
            c.JSON(http.StatusBadRequest, gin.H{"error": "cannot delete your own account"})
            return
        }
    }
    if _, err := h.authSvc.DB.Exec(`DELETE FROM user_groups WHERE user_id = ?`, id); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    if _, err := h.authSvc.DB.Exec(`DELETE FROM users WHERE id = ?`, id); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminSetUserActive enables or disables a user account
func (h *Handlers) AdminSetUserActive(c *gin.Context) {
    idStr := c.Param("id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil || id <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
        return
    }
    activeStr := c.PostForm("active")
    if activeStr == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "missing active value"})
        return
    }
    var activeInt int64
    if activeStr == "1" || strings.EqualFold(activeStr, "true") {
        activeInt = 1
    } else if activeStr == "0" || strings.EqualFold(activeStr, "false") {
        activeInt = 0
    } else {
        c.JSON(http.StatusBadRequest, gin.H{"error": "active must be 0/1 or true/false"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`UPDATE users SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, activeInt, id); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminDisableUserMFA clears mfa_secret to disable MFA
func (h *Handlers) AdminDisableUserMFA(c *gin.Context) {
    idStr := c.Param("id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil || id <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`UPDATE users SET mfa_secret = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, id); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminResetUserMFA resets MFA by clearing secret (user must re-enroll)
func (h *Handlers) AdminResetUserMFA(c *gin.Context) {
    idStr := c.Param("id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil || id <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`UPDATE users SET mfa_secret = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, id); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminEnableUserMFA generates a new MFA secret for the user
func (h *Handlers) AdminEnableUserMFA(c *gin.Context) {
    idStr := c.Param("id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil || id <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
        return
    }
    // Admin enable: set a new secret directly so MFA is immediately enabled
    secret, err := generateBase32Secret()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate secret"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`UPDATE users SET mfa_secret = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, secret, id); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    // Remove any pending enrollment rows
    _, _ = h.authSvc.DB.Exec(`DELETE FROM mfa_enrollments WHERE user_id = ?`, id)
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// generateSecret returns a URL-safe random string
func generateSecret() (string, error) {
    // reuse auth.randomToken-style but local implementation
    b := make([]byte, 20)
    if _, err := io.ReadFull(randReader{}, b); err != nil {
        return "", err
    }
    return base64.RawURLEncoding.EncodeToString(b), nil
}

// randReader wraps crypto/rand.Reader to satisfy io.Reader in a context where imports are at top
type randReader struct{}

func (randReader) Read(p []byte) (int, error) { return rand.Read(p) }

// AdminCreateGroup creates a group
func (h *Handlers) AdminCreateGroup(c *gin.Context) {
    name := strings.TrimSpace(c.PostForm("name"))
    if name == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "group name required"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`INSERT OR IGNORE INTO groups (name) VALUES (?)`, name); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminDeleteGroup deletes a group by id
func (h *Handlers) AdminDeleteGroup(c *gin.Context) {
    idStr := c.Param("id")
    id, err := strconv.ParseInt(idStr, 10, 64)
    if err != nil || id <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid group id"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`DELETE FROM user_groups WHERE group_id = ?`, id); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    if _, err := h.authSvc.DB.Exec(`DELETE FROM groups WHERE id = ?`, id); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminAddUserToGroup links a user to a group
func (h *Handlers) AdminAddUserToGroup(c *gin.Context) {
    userIDStr := c.PostForm("user_id")
    groupIDStr := c.PostForm("group_id")
    userID, err1 := strconv.ParseInt(userIDStr, 10, 64)
    groupID, err2 := strconv.ParseInt(groupIDStr, 10, 64)
    if err1 != nil || err2 != nil || userID <= 0 || groupID <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user_id or group_id"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?, ?)`, userID, groupID); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// AdminRemoveUserFromGroup unlinks a user from a group
func (h *Handlers) AdminRemoveUserFromGroup(c *gin.Context) {
    userIDStr := c.PostForm("user_id")
    groupIDStr := c.PostForm("group_id")
    userID, err1 := strconv.ParseInt(userIDStr, 10, 64)
    groupID, err2 := strconv.ParseInt(groupIDStr, 10, 64)
    if err1 != nil || err2 != nil || userID <= 0 || groupID <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user_id or group_id"})
        return
    }
    if _, err := h.authSvc.DB.Exec(`DELETE FROM user_groups WHERE user_id = ? AND group_id = ?`, userID, groupID); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusOK, gin.H{"success": true})
}

// isAuthenticated returns true if a user_id exists in the Gin context
func isAuthenticated(c *gin.Context) bool {
	_, ok := c.Get("user_id")
	return ok
}

// getUserIDPtr returns a pointer to user_id from context or nil if unauthenticated
func getUserIDPtr(c *gin.Context) *int64 {
	if v, ok := c.Get("user_id"); ok {
		if id, ok2 := v.(int64); ok2 {
			return &id
		}
	}
	return nil
}

// isAdmin returns true if the Gin context has is_admin flag set by middleware
func isAdmin(c *gin.Context) bool {
	_, ok := c.Get("is_admin")
	return ok
}

// EditTextPageHandler renders an editor for allowed text files (json, html, xml, yaml, etc.)
func (h *Handlers) EditTextPageHandler(c *gin.Context) {
	filePath := strings.TrimPrefix(c.Param("path"), "/")

	// Security check
	if strings.Contains(filePath, "..") {
		c.HTML(http.StatusBadRequest, "error", gin.H{
			"error":            "Invalid path",
			"app_name":         h.config.AppName,
			"message":          "Path traversal is not allowed",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Access control
	if allowed, err := h.authSvc.HasReadAccess(getUserIDPtr(c), filePath); err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Permission check failed",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	} else if !allowed {
		c.HTML(http.StatusForbidden, "error", gin.H{
			"error":            "Access denied",
			"app_name":         h.config.AppName,
			"message":          "You do not have permission to view this file",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	fullPath := filepath.Join(h.config.NotesDir, filePath)

	// Ensure file exists
	if _, err := os.Stat(fullPath); os.IsNotExist(err) {
		c.HTML(http.StatusNotFound, "error", gin.H{
			"error":            "File not found",
			"app_name":         h.config.AppName,
			"message":          "The requested file does not exist",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Only allow editing of configured text file types (not markdown here)
	ext := filepath.Ext(fullPath)
	ftype := models.GetFileType(ext, h.config.AllowedImageExtensions, h.config.AllowedFileExtensions)
	if ftype != models.FileTypeText {
		c.HTML(http.StatusForbidden, "error", gin.H{
			"error":            "Editing not allowed",
			"app_name":         h.config.AppName,
			"message":          "This file type cannot be edited here",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Load content
	data, err := os.ReadFile(fullPath)
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to read file",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Build notes tree
	notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config)
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to build notes tree",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	folderPath := filepath.Dir(filePath)
	if folderPath == "." {
		folderPath = ""
	}

	c.HTML(http.StatusOK, "edit_text", gin.H{
		"app_name":        h.config.AppName,
		"title":           filepath.Base(filePath),
		"content":         string(data),
		"file_path":       filePath,
		"file_ext":        strings.TrimPrefix(strings.ToLower(ext), "."),
		"folder_path":     folderPath,
		"notes_tree":      notesTree,
		"active_path":     utils.GetActivePath(folderPath),
		"breadcrumbs":     utils.GenerateBreadcrumbs(folderPath),
		"Authenticated":   isAuthenticated(c),
		"IsAdmin":         isAdmin(c),
		"ContentTemplate": "edit_text_content",
		"ScriptsTemplate": "edit_text_scripts",
		"Page":            "edit_text",
	})
}

// PostEditTextHandler saves changes to an allowed text file
func (h *Handlers) PostEditTextHandler(c *gin.Context) {
	filePath := strings.TrimPrefix(c.Param("path"), "/")

	if strings.Contains(filePath, "..") {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid path"})
		return
	}

	// Access control
	if allowed, err := h.authSvc.HasReadAccess(getUserIDPtr(c), filePath); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Permission check failed"})
		return
	} else if !allowed {
		c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
		return
	}

	fullPath := filepath.Join(h.config.NotesDir, filePath)

	// Enforce allowed file type
	ext := filepath.Ext(fullPath)
	ftype := models.GetFileType(ext, h.config.AllowedImageExtensions, h.config.AllowedFileExtensions)
	if ftype != models.FileTypeText {
		c.JSON(http.StatusForbidden, gin.H{"error": "This file type cannot be edited"})
		return
	}

	content := c.PostForm("content")

	// Ensure parent directory exists
	if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create parent directory"})
		return
	}

	if err := os.WriteFile(fullPath, []byte(content), 0o644); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"success": true, "redirect": h.config.URLPrefix + "/view_text/" + filePath})
}

func New(cfg *config.Config, store *sessions.CookieStore, authSvc *auth.Service) *Handlers {
	return &Handlers{
		config:   cfg,
		store:    store,
		renderer: markdown.NewRenderer(cfg),
		authSvc:  authSvc,
	}
}

func (h *Handlers) IndexHandler(c *gin.Context) {
	fmt.Printf("DEBUG: IndexHandler called\n")

	folderContents, err := utils.GetFolderContents("", h.config)
	if err != nil {
		fmt.Printf("DEBUG: Error getting folder contents: %v\n", err)
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to read directory",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	fmt.Printf("DEBUG: Found %d folder contents\n", len(folderContents))

	notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config)
	if err != nil {
		fmt.Printf("DEBUG: Error building tree structure: %v\n", err)
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to build tree structure",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	fmt.Printf("DEBUG: Tree structure built, app_name: %s\n", h.config.AppName)

	// Access control
	if allowed, err := h.authSvc.HasReadAccess(getUserIDPtr(c), ""); err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Permission check failed",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	} else if !allowed {
		c.HTML(http.StatusForbidden, "error", gin.H{
			"error":            "Access denied",
			"app_name":         h.config.AppName,
			"message":          "You do not have permission to view this folder",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	c.HTML(http.StatusOK, "folder", gin.H{
		"app_name":                 h.config.AppName,
		"folder_path":              "",
		"folder_contents":          folderContents,
		"notes_tree":               notesTree,
		"active_path":              []string{},
		"current_note":             nil,
		"breadcrumbs":              utils.GenerateBreadcrumbs(""),
		"allowed_image_extensions": h.config.AllowedImageExtensions,
		"allowed_file_extensions":  h.config.AllowedFileExtensions,
		"Authenticated":            isAuthenticated(c),
		"IsAdmin":                  isAdmin(c),
		"ContentTemplate":          "folder_content",
		"ScriptsTemplate":          "folder_scripts",
		"Page":                     "folder",
	})
}

func (h *Handlers) FolderHandler(c *gin.Context) {
	folderPath := strings.TrimPrefix(c.Param("path"), "/")

	// Security check - prevent path traversal
	if strings.Contains(folderPath, "..") {
		c.HTML(http.StatusBadRequest, "error", gin.H{
			"error":            "Invalid path",
			"app_name":         h.config.AppName,
			"message":          "Path traversal is not allowed",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Check if path is in skipped directories
	if utils.IsPathInSkippedDirs(folderPath, h.config.NotesDirSkip) {
		c.HTML(http.StatusForbidden, "error", gin.H{
			"error":            "Access denied",
			"app_name":         h.config.AppName,
			"message":          "This directory is not accessible",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Access control
	if allowed, err := h.authSvc.HasReadAccess(getUserIDPtr(c), folderPath); err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Permission check failed",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	} else if !allowed {
		c.HTML(http.StatusForbidden, "error", gin.H{
			"error":            "Access denied",
			"app_name":         h.config.AppName,
			"message":          "You do not have permission to view this folder",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	folderContents, err := utils.GetFolderContents(folderPath, h.config)
	if err != nil {
		c.HTML(http.StatusNotFound, "error", gin.H{
			"error":            "Folder not found",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config)
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to build tree structure",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	c.HTML(http.StatusOK, "folder", gin.H{
		"app_name":                 h.config.AppName,
		"folder_path":              folderPath,
		"folder_contents":          folderContents,
		"notes_tree":               notesTree,
		"active_path":              utils.GetActivePath(folderPath),
		"current_note":             nil,
		"breadcrumbs":              utils.GenerateBreadcrumbs(folderPath),
		"allowed_image_extensions": h.config.AllowedImageExtensions,
		"allowed_file_extensions":  h.config.AllowedFileExtensions,
		"Authenticated":            isAuthenticated(c),
		"IsAdmin":                  isAdmin(c),
		"ContentTemplate":          "folder_content",
		"ScriptsTemplate":          "folder_scripts",
		"Page":                     "folder",
	})
}

func (h *Handlers) NoteHandler(c *gin.Context) {
	notePath := strings.TrimPrefix(c.Param("path"), "/")

	if !strings.HasSuffix(notePath, ".md") {
		c.HTML(http.StatusBadRequest, "error", gin.H{
			"error":            "Invalid note path",
			"app_name":         h.config.AppName,
			"message":          "Note path must end with .md",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Security check
	if strings.Contains(notePath, "..") {
		c.HTML(http.StatusBadRequest, "error", gin.H{
			"error":            "Invalid path",
			"app_name":         h.config.AppName,
			"message":          "Path traversal is not allowed",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Check if path is in skipped directories
	if utils.IsPathInSkippedDirs(notePath, h.config.NotesDirSkip) {
		c.HTML(http.StatusForbidden, "error", gin.H{
			"error":            "Access denied",
			"app_name":         h.config.AppName,
			"message":          "This note is not accessible",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Access control
	if allowed, err := h.authSvc.HasReadAccess(getUserIDPtr(c), notePath); err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Permission check failed",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	} else if !allowed {
		c.HTML(http.StatusForbidden, "error", gin.H{
			"error":            "Access denied",
			"app_name":         h.config.AppName,
			"message":          "You do not have permission to view this note",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	fullPath := filepath.Join(h.config.NotesDir, notePath)

	if _, err := os.Stat(fullPath); os.IsNotExist(err) {
		c.HTML(http.StatusNotFound, "error", gin.H{
			"error":            "Note not found",
			"app_name":         h.config.AppName,
			"message":          "The requested note does not exist",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	content, err := os.ReadFile(fullPath)
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to read note",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	htmlContent, err := h.renderer.RenderMarkdown(string(content), notePath)
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to render markdown",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config)
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to build tree structure",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	title := strings.TrimSuffix(filepath.Base(notePath), ".md")
	folderPath := filepath.Dir(notePath)
	if folderPath == "." {
		folderPath = ""
	}

	c.HTML(http.StatusOK, "note", gin.H{
		"app_name":     h.config.AppName,
		"title":        title,
		"content":      htmlContent,
		"note_path":    notePath,
		"folder_path":  folderPath,
		"notes_tree":   notesTree,
		"active_path":  utils.GetActivePath(folderPath),
		"current_note": notePath,
		"breadcrumbs":  utils.GenerateBreadcrumbs(folderPath),
		"Authenticated":   isAuthenticated(c),
		"IsAdmin":         isAdmin(c),
		"ContentTemplate": "note_content",
		"ScriptsTemplate": "note_scripts",
		"Page":            "note",
	})
}

func (h *Handlers) ServeAttachedImageHandler(c *gin.Context) {
	imagePath := strings.TrimPrefix(c.Param("path"), "/")

	// Security check
	if strings.Contains(imagePath, "..") {
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	var fullPath string

	switch h.config.ImageStorageMode {
	case 1: // Root directory
		fullPath = filepath.Join(h.config.NotesDir, imagePath)
	case 3: // Same as note directory
		fullPath = filepath.Join(h.config.NotesDir, imagePath)
	case 4: // Subfolder of note directory
		fullPath = filepath.Join(h.config.NotesDir, imagePath)
	default:
		c.AbortWithStatus(http.StatusNotFound)
		return
	}

	// Check if file exists and is an image
	if _, err := os.Stat(fullPath); os.IsNotExist(err) {
		c.AbortWithStatus(http.StatusNotFound)
		return
	}

	// Access control
	if allowed, err := h.authSvc.HasReadAccess(getUserIDPtr(c), imagePath); err != nil {
		c.AbortWithStatus(http.StatusInternalServerError)
		return
	} else if !allowed {
		c.AbortWithStatus(http.StatusForbidden)
		return
	}

	if !models.IsImageFile(filepath.Base(imagePath), h.config.AllowedImageExtensions) {
		c.AbortWithStatus(http.StatusForbidden)
		return
	}

	c.File(fullPath)
}

func (h *Handlers) ServeStoredImageHandler(c *gin.Context) {
	filename := c.Param("filename")

	// Security check
	if strings.Contains(filename, "..") || strings.Contains(filename, "/") {
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	if h.config.ImageStorageMode != 2 {
		c.AbortWithStatus(http.StatusNotFound)
		return
	}

	fullPath := filepath.Join(h.config.ImageStoragePath, filename)

	if _, err := os.Stat(fullPath); os.IsNotExist(err) {
		c.AbortWithStatus(http.StatusNotFound)
		return
	}

	if !models.IsImageFile(filename, h.config.AllowedImageExtensions) {
		c.AbortWithStatus(http.StatusForbidden)
		return
	}

	// Access control (stored images referenced by pathless filenames are assumed public unless permissions exist for the referencing path)
	// We cannot infer the note path here, so we allow by default per policy.

	c.File(fullPath)
}

func (h *Handlers) DownloadHandler(c *gin.Context) {
	filePath := strings.TrimPrefix(c.Param("path"), "/")

	// Security check
	if strings.Contains(filePath, "..") {
		c.AbortWithStatus(http.StatusBadRequest)
		return
	}

	// Access control
	if allowed, err := h.authSvc.HasReadAccess(getUserIDPtr(c), filePath); err != nil {
		c.AbortWithStatus(http.StatusInternalServerError)
		return
	} else if !allowed {
		c.AbortWithStatus(http.StatusForbidden)
		return
	}

	fullPath := filepath.Join(h.config.NotesDir, filePath)

	if _, err := os.Stat(fullPath); os.IsNotExist(err) {
		c.AbortWithStatus(http.StatusNotFound)
		return
	}

	filename := filepath.Base(filePath)
	c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
	c.File(fullPath)
}

func (h *Handlers) ViewTextHandler(c *gin.Context) {
	filePath := strings.TrimPrefix(c.Param("path"), "/")

	// Security check
	if strings.Contains(filePath, "..") {
		c.HTML(http.StatusBadRequest, "error", gin.H{
			"error":            "Invalid path",
			"app_name":         h.config.AppName,
			"message":          "Path traversal is not allowed",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Access control
	if allowed, err := h.authSvc.HasReadAccess(getUserIDPtr(c), filePath); err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Permission check failed",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	} else if !allowed {
		c.HTML(http.StatusForbidden, "error", gin.H{
			"error":            "Access denied",
			"app_name":         h.config.AppName,
			"message":          "You do not have permission to view this file",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	fullPath := filepath.Join(h.config.NotesDir, filePath)

	if _, err := os.Stat(fullPath); os.IsNotExist(err) {
		c.HTML(http.StatusNotFound, "error", gin.H{
			"error":            "File not found",
			"app_name":         h.config.AppName,
			"message":          "The requested file does not exist",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	// Check if file extension is allowed
	if !models.IsAllowedFile(filePath, h.config.AllowedFileExtensions) {
		c.HTML(http.StatusForbidden, "error", gin.H{
			"error":            "File type not allowed",
			"app_name":         h.config.AppName,
			"message":          "This file type cannot be viewed",
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	content, err := os.ReadFile(fullPath)
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to read file",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config)
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error", gin.H{
			"error":            "Failed to build tree structure",
			"app_name":         h.config.AppName,
			"message":          err.Error(),
			"ContentTemplate":  "error_content",
			"ScriptsTemplate":  "error_scripts",
			"Page":             "error",
		})
		return
	}

	folderPath := filepath.Dir(filePath)
	if folderPath == "." {
		folderPath = ""
	}

	// Determine extension and whether file is editable as text
	ext := filepath.Ext(filePath)
	ftype := models.GetFileType(ext, h.config.AllowedImageExtensions, h.config.AllowedFileExtensions)
	isEditable := ftype == models.FileTypeText

	c.HTML(http.StatusOK, "view_text", gin.H{
		"app_name":    h.config.AppName,
		"file_name":   filepath.Base(filePath),
		"file_path":   filePath,
		"content":     string(content),
		"file_ext":    strings.TrimPrefix(strings.ToLower(ext), "."),
		"is_editable": isEditable,
		"folder_path": folderPath,
		"notes_tree":  notesTree,
		"active_path": utils.GetActivePath(folderPath),
		"breadcrumbs": utils.GenerateBreadcrumbs(folderPath),
		"Authenticated":   isAuthenticated(c),
		"IsAdmin":         isAdmin(c),
		"ContentTemplate": "view_text_content",
		"ScriptsTemplate": "view_text_scripts",
		"Page":            "view_text",
	})
}

func (h *Handlers) UploadHandler(c *gin.Context) {
	// Parse multipart form
	if err := c.Request.ParseMultipartForm(h.config.MaxContentLength); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "File too large or invalid form data"})
		return
	}

	// Get the upload path
	uploadPath := c.PostForm("path")
	if uploadPath == "" {
		uploadPath = ""
	}

	// Security check
	if strings.Contains(uploadPath, "..") {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid upload path"})
		return
	}

	file, header, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
		return
	}
	defer file.Close()

	// Validate file type
	buffer := make([]byte, 512)
	if _, err := file.Read(buffer); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read file"})
		return
	}
	file.Seek(0, 0) // Reset file pointer

	kind, _ := filetype.Match(buffer)
	if kind == filetype.Unknown {
		// Allow text files and other allowed extensions
		if !models.IsAllowedFile(header.Filename, append(h.config.AllowedImageExtensions, h.config.AllowedFileExtensions...)) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "File type not allowed"})
			return
		}
	} else {
		// Check if detected type is allowed
		isImageType := strings.HasPrefix(kind.MIME.Value, "image/")
		if !isImageType && !models.IsAllowedFile(header.Filename, h.config.AllowedFileExtensions) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "File type not allowed"})
			return
		}
	}

	// Determine upload directory
	var uploadDir string
	isImage := models.IsImageFile(header.Filename, h.config.AllowedImageExtensions)

	if isImage {
		// For images, use the configured storage mode
		storageInfo := utils.GetImageStorageInfo(uploadPath, h.config)
		uploadDir = storageInfo.StorageDir
	} else {
		// For other files, upload to the current folder
		uploadDir = filepath.Join(h.config.NotesDir, uploadPath)
	}

	// Ensure upload directory exists
	if err := utils.EnsureDir(uploadDir); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upload directory"})
		return
	}

	// Create destination file
	destPath := filepath.Join(uploadDir, header.Filename)
	dest, err := os.Create(destPath)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create destination file"})
		return
	}
	defer dest.Close()

	// Copy file content
	if _, err := io.Copy(dest, file); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"success":  true,
		"message":  "File uploaded successfully",
		"filename": header.Filename,
		"size":     header.Size,
	})
}

func (h *Handlers) TreeAPIHandler(c *gin.Context) {
	notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, notesTree)
}

// AdminPage renders a simple admin dashboard listing users, groups, and permissions
func (h *Handlers) AdminPage(c *gin.Context) {
	// Query users
	users := make([]auth.User, 0, 32)
	if rows, err := h.authSvc.DB.Query(`SELECT id, username, email, password_hash, is_active, email_confirmed, mfa_secret, created_at, updated_at FROM users ORDER BY username`); err == nil {
		defer rows.Close()
		for rows.Next() {
			var u auth.User
			var mfa sql.NullString
			if err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &u.IsActive, &u.EmailConfirmed, &mfa, &u.CreatedAt, &u.UpdatedAt); err == nil {
				u.MFASecret = mfa
				users = append(users, u)
			}
		}
	}

	// Query groups
	type Group struct{ ID int64; Name string }
	groups := make([]Group, 0, 16)
	if gr, err := h.authSvc.DB.Query(`SELECT id, name FROM groups ORDER BY name`); err == nil {
		defer gr.Close()
		for gr.Next() {
			var g Group
			if err := gr.Scan(&g.ID, &g.Name); err == nil {
				groups = append(groups, g)
			}
		}
	}

	// Query permissions
	type Permission struct {
		Group     string
		Path      string
		CanRead   bool
		CanWrite  bool
		CanDelete bool
	}
	perms := make([]Permission, 0, 64)
	if pr, err := h.authSvc.DB.Query(`
		SELECT g.name, p.path_prefix, p.can_read, p.can_write, p.can_delete
		FROM permissions p
		JOIN groups g ON g.id = p.group_id
		ORDER BY g.name, p.path_prefix`); err == nil {
		defer pr.Close()
		for pr.Next() {
			var name, path string
			var r, w, d int
			if err := pr.Scan(&name, &path, &r, &w, &d); err == nil {
				perms = append(perms, Permission{Group: name, Path: path, CanRead: r == 1, CanWrite: w == 1, CanDelete: d == 1})
			}
		}
	}

	// Build tree for sidebar
	notesTree, _ := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config)

    // current user id for UI restrictions (e.g., prevent self-delete)
    var currentUserID int64
    if v, ok := c.Get("user_id"); ok {
        if id, ok2 := v.(int64); ok2 {
            currentUserID = id
        }
    }

	c.HTML(http.StatusOK, "admin", gin.H{
		"app_name":        h.config.AppName,
		"notes_tree":      notesTree,
		"active_path":     []string{},
		"current_note":    nil,
		"breadcrumbs":     utils.GenerateBreadcrumbs(""),
		"Authenticated":   true,
		"IsAdmin":         true,
		"users":           users,
		"groups":          groups,
		"permissions":     perms,
		"CurrentUserID":   currentUserID,
		"ContentTemplate": "admin_content",
		"ScriptsTemplate": "admin_scripts",
		"Page":            "admin",
	})
}

// SearchHandler performs a simple full-text search across markdown and allowed text files
// within the notes directory, honoring skipped directories.
// GET /api/search?q=term
func (h *Handlers) SearchHandler(c *gin.Context) {
	query := strings.TrimSpace(c.Query("q"))
	if query == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "missing query"})
		return
	}

	// Case-insensitive search
	qLower := strings.ToLower(query)

	type Snippet struct {
		Line    int    `json:"line"`
		Preview string `json:"preview"`
	}
	type Result struct {
		Path     string    `json:"path"`
		Type     string    `json:"type"` // "md" or "text"
		Snippets []Snippet `json:"snippets"`
	}

	results := make([]Result, 0, 32)

	// Walk the notes directory
	err := filepath.Walk(h.config.NotesDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // skip on error
		}
		if info.IsDir() {
			// Compute relative dir path to check skip list
			rel, _ := filepath.Rel(h.config.NotesDir, path)
			rel = filepath.ToSlash(rel)
			if rel == "." {
				rel = ""
			}
			if utils.IsPathInSkippedDirs(rel, h.config.NotesDirSkip) {
				return filepath.SkipDir
			}
			return nil
		}

		// Compute relative file path
		relPath, _ := filepath.Rel(h.config.NotesDir, path)
		relPath = filepath.ToSlash(relPath)

		// Skip disallowed files
		ext := strings.ToLower(filepath.Ext(relPath))
		isMD := ext == ".md"
		if !isMD && !models.IsAllowedFile(relPath, h.config.AllowedFileExtensions) {
			return nil
		}

		// Read file content (limit size to prevent huge memory usage)
		data, readErr := os.ReadFile(path)
		if readErr != nil {
			return nil
		}
		content := string(data)
		contentLower := strings.ToLower(content)
		if !strings.Contains(contentLower, qLower) {
			return nil
		}

		// Build snippets: show up to 3 matches with 2 lines of context
		lines := strings.Split(content, "\n")
		linesLower := strings.Split(contentLower, "\n")
		snippets := make([]Snippet, 0, 3)
		for i := 0; i < len(linesLower) && len(snippets) < 3; i++ {
			if strings.Contains(linesLower[i], qLower) {
				start := i - 2
				if start < 0 {
					start = 0
				}
				end := i + 2
				if end >= len(lines) {
					end = len(lines) - 1
				}
				// Join preview lines
				preview := strings.Join(lines[start:end+1], "\n")
				snippets = append(snippets, Snippet{Line: i + 1, Preview: preview})
			}
		}

		rtype := "text"
		if isMD {
			rtype = "md"
		}
		results = append(results, Result{Path: relPath, Type: rtype, Snippets: snippets})
		return nil
	})

	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"query": query, "results": results})
}
