package handlers

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"golang.org/x/crypto/bcrypt"

	"chat-switchboard/auth"
	"chat-switchboard/crypto"
	"chat-switchboard/database"
	"chat-switchboard/models"
	"chat-switchboard/providers"
	"chat-switchboard/storage"
	"chat-switchboard/store"
	"chat-switchboard/tools/search"
)

type AdminHandler struct {
	stores        store.Stores
	vault         *crypto.KeyResolver
	uekCache      *crypto.UEKCache
	objStore      storage.ObjectStore
	onUserChanged func(userID string) // auth cache eviction callback
}

func NewAdminHandler(s store.Stores, vault *crypto.KeyResolver, uekCache *crypto.UEKCache, objStore storage.ObjectStore) *AdminHandler {
	return &AdminHandler{stores: s, vault: vault, uekCache: uekCache, objStore: objStore}
}

// OnUserChanged registers a callback invoked when a user's role or
// active status changes. Used to evict the auth middleware cache.
func (h *AdminHandler) OnUserChanged(fn func(userID string)) {
	h.onUserChanged = fn
}

// ── User Management ─────────────────────────

func (h *AdminHandler) ListUsers(c *gin.Context) {
	opts := store.DefaultListOptions()

	// Accept both limit/offset and page/per_page conventions
	if limit, _ := strconv.Atoi(c.Query("limit")); limit > 0 {
		opts.Limit = limit
	}
	if offset, _ := strconv.Atoi(c.Query("offset")); offset > 0 {
		opts.Offset = offset
	}
	if perPage, _ := strconv.Atoi(c.Query("per_page")); perPage > 0 {
		opts.Limit = perPage
	}
	if page, _ := strconv.Atoi(c.Query("page")); page > 1 {
		opts.Offset = (page - 1) * opts.Limit
	}

	users, total, err := h.stores.Users.List(c.Request.Context(), opts)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list users"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"data": users, "total": total})
}

func (h *AdminHandler) CreateUser(c *gin.Context) {
	var req struct {
		Username string `json:"username" binding:"required"`
		Email    string `json:"email" binding:"required"`
		Password string `json:"password" binding:"required,min=8"`
		Role     string `json:"role"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcryptCost)
	role := models.UserRoleUser
	if req.Role == models.UserRoleAdmin {
		role = models.UserRoleAdmin
	}

	user := &models.User{
		Username:     strings.ToLower(req.Username),
		Email:        strings.ToLower(req.Email),
		PasswordHash: string(hash),
		Role:         role,
		IsActive:     true,
		Handle:       auth.UniqueHandle(c.Request.Context(), h.stores.Users, models.HandleFromName(req.Username)),
	}

	if err := h.stores.Users.Create(c.Request.Context(), user); err != nil {
		if database.IsUniqueViolation(err) {
			c.JSON(http.StatusConflict, gin.H{"error": "username or email already exists"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"})
		return
	}

	h.auditLog(c, "user.create", "user", user.ID, nil)
	c.JSON(http.StatusCreated, user)
}

func (h *AdminHandler) UpdateUserRole(c *gin.Context) {
	id := c.Param("id")
	var req struct {
		Role string `json:"role" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Guard: prevent demoting the last admin
	if req.Role != models.UserRoleAdmin {
		user, err := h.stores.Users.GetByID(c.Request.Context(), id)
		if err != nil || user == nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
			return
		}
		if user.Role == models.UserRoleAdmin {
			adminCount, _ := h.stores.Users.CountByRole(c.Request.Context(), models.UserRoleAdmin)
			if adminCount <= 1 {
				c.JSON(http.StatusConflict, gin.H{"error": "cannot demote the last admin"})
				return
			}
		}
	}

	if err := h.stores.Users.Update(c.Request.Context(), id, map[string]interface{}{"role": req.Role}); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update role"})
		return
	}

	h.auditLog(c, "user.role_change", "user", id, gin.H{"role": req.Role})
	if h.onUserChanged != nil {
		h.onUserChanged(id)
	}
	c.JSON(http.StatusOK, gin.H{"message": "role updated"})
}

func (h *AdminHandler) ToggleUserActive(c *gin.Context) {
	id := c.Param("id")
	var req struct {
		IsActive bool `json:"is_active"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	if err := h.stores.Users.SetActive(c.Request.Context(), id, req.IsActive); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update status"})
		return
	}

	h.auditLog(c, "user.active_change", "user", id, gin.H{"is_active": req.IsActive})
	if h.onUserChanged != nil {
		h.onUserChanged(id)
	}
	c.JSON(http.StatusOK, gin.H{"message": "user status updated"})
}

func (h *AdminHandler) ResetPassword(c *gin.Context) {
	id := c.Param("id")
	var req struct {
		Password string `json:"password" binding:"required,min=8"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcryptCost)
	if err := h.stores.Users.Update(c.Request.Context(), id, map[string]interface{}{"password_hash": string(hash)}); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reset password"})
		return
	}

	// Destroy vault — old UEK is unrecoverable with new password.
	// A fresh vault will be initialized on the user's next login.
	h.destroyVault(c, id)

	h.auditLog(c, "user.password_reset", "user", id, nil)
	c.JSON(http.StatusOK, gin.H{"message": "password reset"})
}

// destroyVault nullifies a user's encrypted UEK and deletes their personal
// provider keys. Called when an admin resets a user's password — the old
// UEK cannot be recovered without the old password.
func (h *AdminHandler) destroyVault(c *gin.Context, userID string) {
	if h.uekCache == nil {
		return
	}

	deleted := DestroyVaultDB(c.Request.Context(), h.stores, userID)

	// Evict from session cache (if user is currently logged in)
	h.uekCache.Evict(userID)

	if deleted > 0 {
		log.Printf("  🔐 Destroyed %d personal provider(s) for user %s", deleted, userID)
	}

	h.auditLog(c, "user.vault_destroyed", "user", userID, models.JSONMap{
		"reason": "admin_password_reset",
	})
}

// ResetVault explicitly resets a user's vault without changing their password.
// Clears vault columns, deletes personal provider configs, evicts UEK cache.
// The user gets a fresh vault on their next login.
// POST /api/v1/admin/users/:id/vault/reset
func (h *AdminHandler) ResetVault(c *gin.Context) {
	userID := c.Param("id")

	// Verify user exists
	user, err := h.stores.Users.GetByID(c.Request.Context(), userID)
	if err != nil || user == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
		return
	}

	if h.uekCache == nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "vault not configured"})
		return
	}

	deleted := DestroyVaultDB(c.Request.Context(), h.stores, userID)
	h.uekCache.Evict(userID)

	h.auditLog(c, "user.vault_reset", "user", userID, models.JSONMap{
		"reason":            "admin_manual_reset",
		"providers_deleted": deleted,
	})

	log.Printf("  🔐 Vault reset for user %s (%d personal provider(s) cleared)", userID, deleted)
	c.JSON(http.StatusOK, gin.H{
		"message":            "vault reset — user will get a fresh vault on next login",
		"providers_deleted":  deleted,
	})
}

func (h *AdminHandler) DeleteUser(c *gin.Context) {
	id := c.Param("id")

	// Guard: prevent deleting the last admin
	user, err := h.stores.Users.GetByID(c.Request.Context(), id)
	if err != nil || user == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
		return
	}
	if user.Role == models.UserRoleAdmin {
		adminCount, _ := h.stores.Users.CountByRole(c.Request.Context(), models.UserRoleAdmin)
		if adminCount <= 1 {
			c.JSON(http.StatusConflict, gin.H{"error": "cannot delete the last admin"})
			return
		}
	}

	// Destroy vault and personal BYOK keys before deleting the user row
	h.destroyVault(c, id)

	if err := h.stores.Users.Delete(c.Request.Context(), id); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete user"})
		return
	}
	h.auditLog(c, "user.delete", "user", id, nil)
	c.JSON(http.StatusOK, gin.H{"message": "user deleted"})
}

// ── Global Settings ─────────────────────────

func (h *AdminHandler) ListGlobalSettings(c *gin.Context) {
	settings, err := h.stores.GlobalConfig.GetAll(c.Request.Context())
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load settings"})
		return
	}
	// Also include policies
	policies, _ := h.stores.Policies.GetAll(c.Request.Context())
	c.JSON(http.StatusOK, gin.H{"settings": settings, "policies": policies})
}

func (h *AdminHandler) GetGlobalSetting(c *gin.Context) {
	key := c.Param("key")

	// If the key is a known policy, read from the policies table first
	if _, ok := models.PolicyDefaults[key]; ok {
		val, err := h.stores.Policies.Get(c.Request.Context(), key)
		if err == nil {
			c.JSON(http.StatusOK, gin.H{"key": key, "value": val})
			return
		}
		// Fall through to global_config if not found in policies
	}

	val, err := h.stores.GlobalConfig.Get(c.Request.Context(), key)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "setting not found"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"key": key, "value": val})
}

func (h *AdminHandler) UpdateGlobalSetting(c *gin.Context) {
	key := c.Param("key")
	userID, _ := c.Get("user_id")
	uid := userID.(string)

	var req struct {
		Value json.RawMessage `json:"value"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Determine if this is a policy (string value) or a global config (JSON value)
	var strVal string
	if err := json.Unmarshal(req.Value, &strVal); err == nil {
		// String value → try as policy first
		if _, ok := models.PolicyDefaults[key]; ok {
			if err := h.stores.Policies.Set(c.Request.Context(), key, strVal, uid); err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update policy"})
				return
			}
			h.auditLog(c, "policy.update", "policy", key, gin.H{"value": strVal})
			c.JSON(http.StatusOK, gin.H{"message": "policy updated"})
			return
		}
	}

	// JSON value → global config
	var jsonVal models.JSONMap
	json.Unmarshal(req.Value, &jsonVal)
	if err := h.stores.GlobalConfig.Set(c.Request.Context(), key, jsonVal, uid); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update setting"})
		return
	}

	h.auditLog(c, "settings.update", "global_settings", key, nil)

	// Live-apply hooks for settings that have runtime effects
	if key == "search_config" {
		applySearchConfig(jsonVal)
	}

	c.JSON(http.StatusOK, gin.H{"message": "setting updated"})
}

// applySearchConfig updates the active search provider at runtime.
func applySearchConfig(raw models.JSONMap) {
	b, err := json.Marshal(raw)
	if err != nil {
		log.Printf("⚠️ Failed to marshal search config: %v", err)
		return
	}
	var cfg search.Config
	if err := json.Unmarshal(b, &cfg); err != nil {
		log.Printf("⚠️ Failed to parse search config: %v", err)
		return
	}
	if err := search.ApplyConfig(cfg); err != nil {
		log.Printf("⚠️ Failed to apply search config: %v", err)
	}
}

func (h *AdminHandler) PublicSettings(c *gin.Context) {
	// Banner config, branding, etc. — safe subset for non-admin users
	banner, _ := h.stores.GlobalConfig.Get(c.Request.Context(), "banner")
	branding, _ := h.stores.GlobalConfig.Get(c.Request.Context(), "branding")
	systemPrompt, _ := h.stores.GlobalConfig.Get(c.Request.Context(), "system_prompt")
	policies, _ := h.stores.Policies.GetAll(c.Request.Context())

	// Paste-to-file threshold (admin-configurable via storage.paste_to_file_chars, default 2000)
	pasteChars := 2000
	if storageSettings, err := h.stores.GlobalConfig.Get(c.Request.Context(), "storage"); err == nil {
		if v, ok := storageSettings["paste_to_file_chars"]; ok {
			switch n := v.(type) {
			case float64:
				pasteChars = int(n)
			case int:
				pasteChars = n
			}
		}
	}

	// Only tell the user whether admin prompt exists — don't expose content
	hasAdminPrompt := false
	if content, ok := systemPrompt["content"].(string); ok && content != "" {
		hasAdminPrompt = true
	}

	// Retention TTL (v0.37.14 — days before purge for global/team channels)
	retentionTTL := 0
	if ttlCfg, err := h.stores.GlobalConfig.Get(c.Request.Context(), "retention_ttl_days"); err == nil {
		if v, ok := ttlCfg["value"].(float64); ok {
			retentionTTL = int(v)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"banner":              banner,
		"branding":            branding,
		"has_admin_prompt":    hasAdminPrompt,
		"storage_configured":  storageConfigured,
		"paste_to_file_chars": pasteChars,
		"policies": gin.H{
			"allow_registration":      policies["allow_registration"],
			"allow_user_byok":         policies["allow_user_byok"],
			"allow_user_personas":     policies["allow_user_personas"],
			"retention_ttl_days":      retentionTTL,
		},
	})
}

// ── Vault Status ────────────────────────────

func (h *AdminHandler) VaultStatus(c *gin.Context) {
	hasKey := h.vault != nil && h.vault.HasEnvKey()
	keyStr := ""
	if hasKey {
		keyStr = "set" // non-empty signals to VaultStatus that key is configured
	}
	status, err := crypto.VaultStatus(database.DB, keyStr)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get vault status"})
		return
	}
	c.JSON(http.StatusOK, status)
}

// ── Provider Configs (Global) ───────────────

func (h *AdminHandler) ListGlobalConfigs(c *gin.Context) {
	cfgs, err := h.stores.Providers.ListGlobal(c.Request.Context())
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list configs"})
		return
	}

	// Redact API keys but expose has_key flag for UI
	type configWithKey struct {
		models.ProviderConfig
		HasKey bool `json:"has_key"`
	}
	out := make([]configWithKey, len(cfgs))
	for i, cfg := range cfgs {
		out[i] = configWithKey{
			ProviderConfig: cfg,
			HasKey:         cfg.HasKey(),
		}
	}
	c.JSON(http.StatusOK, gin.H{"data": out})
}

func (h *AdminHandler) CreateGlobalConfig(c *gin.Context) {
	// Wrapper struct: models.ProviderConfig has APIKeyEnc tagged json:"-"
	// so ShouldBindJSON would silently drop the api_key field.
	var req struct {
		Name         string                 `json:"name" binding:"required"`
		Provider     string                 `json:"provider" binding:"required"`
		Endpoint     string                 `json:"endpoint" binding:"required"`
		APIKey       string                 `json:"api_key"`
		ModelDefault string                 `json:"model_default,omitempty"`
		Config       map[string]interface{} `json:"config,omitempty"`
		Headers      map[string]interface{} `json:"headers,omitempty"`
		Settings     map[string]interface{} `json:"settings,omitempty"`
		IsPrivate    bool                   `json:"is_private,omitempty"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	if _, err := providers.Get(req.Provider); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error":               "unsupported provider: " + req.Provider,
			"supported_providers": providers.List(),
		})
		return
	}

	cfg := &models.ProviderConfig{
		Name:         req.Name,
		Provider:     req.Provider,
		Endpoint:     req.Endpoint,
		ModelDefault: req.ModelDefault,
		Config:       models.JSONMap(req.Config),
		Headers:      models.JSONMap(req.Headers),
		Settings:     models.JSONMap(req.Settings),
		Scope:        models.ScopeGlobal,
		KeyScope:     models.ScopeGlobal,
		IsActive:     true,
		IsPrivate:    req.IsPrivate,
	}

	// Encrypt the API key
	if req.APIKey != "" {
		if h.vault != nil {
			enc, nonce, err := h.vault.EncryptForScope(req.APIKey, "global", "")
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"})
				return
			}
			cfg.APIKeyEnc = enc
			cfg.KeyNonce = nonce
		} else {
			// No vault configured — store raw bytes (unencrypted fallback)
			cfg.APIKeyEnc = []byte(req.APIKey)
		}
	}

	if err := h.stores.Providers.Create(c.Request.Context(), cfg); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create config"})
		return
	}

	h.auditLog(c, "provider.create", "provider_config", cfg.ID, gin.H{"name": cfg.Name, "provider": cfg.Provider})
	c.JSON(http.StatusCreated, gin.H{
		"id":            cfg.ID,
		"scope":         cfg.Scope,
		"name":          cfg.Name,
		"provider":      cfg.Provider,
		"endpoint":      cfg.Endpoint,
		"model_default": cfg.ModelDefault,
		"config":        cfg.Config,
		"headers":       cfg.Headers,
		"settings":      cfg.Settings,
		"is_active":     cfg.IsActive,
		"is_private":    cfg.IsPrivate,
		"has_key":       cfg.HasKey(),
		"created_at":    cfg.CreatedAt,
		"updated_at":    cfg.UpdatedAt,
	})
}

func (h *AdminHandler) UpdateGlobalConfig(c *gin.Context) {
	id := c.Param("id")

	// Wrapper struct: ProviderConfigPatch has APIKeyEnc tagged json:"-"
	var req struct {
		models.ProviderConfigPatch
		APIKey *string `json:"api_key,omitempty"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	patch := req.ProviderConfigPatch
	// Transfer api_key → encrypted fields
	if req.APIKey != nil && *req.APIKey != "" {
		if h.vault != nil {
			enc, nonce, err := h.vault.EncryptForScope(*req.APIKey, "global", "")
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"})
				return
			}
			patch.APIKeyEnc = enc
			patch.KeyNonce = nonce
		} else {
			patch.APIKeyEnc = []byte(*req.APIKey)
		}
	}

	if err := h.stores.Providers.Update(c.Request.Context(), id, patch); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update config"})
		return
	}

	h.auditLog(c, "provider.update", "provider_config", id, nil)
	c.JSON(http.StatusOK, gin.H{"message": "config updated"})
}

func (h *AdminHandler) DeleteGlobalConfig(c *gin.Context) {
	id := c.Param("id")
	// Delete associated catalog entries first
	h.stores.Catalog.DeleteForProvider(c.Request.Context(), id)
	if err := h.stores.Providers.Delete(c.Request.Context(), id); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete config"})
		return
	}
	h.auditLog(c, "provider.delete", "provider_config", id, nil)
	c.JSON(http.StatusOK, gin.H{"message": "config deleted"})
}

// ── Model Catalog ───────────────────────────

func (h *AdminHandler) ListModelConfigs(c *gin.Context) {
	entries, err := h.stores.Catalog.ListAllGlobal(c.Request.Context())
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list models"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": entries})
}

func (h *AdminHandler) FetchModels(c *gin.Context) {
	var req struct {
		ProviderConfigID string `json:"provider_config_id"`
	}
	c.ShouldBindJSON(&req)

	// If no specific provider, fetch from ALL global providers
	if req.ProviderConfigID == "" {
		configs, err := h.stores.Providers.ListGlobal(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list providers"})
			return
		}
		if len(configs) == 0 {
			c.JSON(http.StatusBadRequest, gin.H{"error": "no providers configured — add one first"})
			return
		}

		totalAdded, totalUpdated, totalFetched := 0, 0, 0
		var errs []string
		for _, cfg := range configs {
			if !cfg.IsActive {
				continue
			}
			added, updated, fetched, err := h.fetchModelsForProvider(c, &cfg)
			if err != nil {
				errs = append(errs, cfg.Provider+": "+err.Error())
				continue
			}
			totalAdded += added
			totalUpdated += updated
			totalFetched += fetched
		}

		result := gin.H{
			"message": "models synced",
			"added":   totalAdded,
			"updated": totalUpdated,
			"total":   totalFetched,
		}
		if len(errs) > 0 {
			result["errors"] = errs
		}
		c.JSON(http.StatusOK, result)
		return
	}

	// Single provider fetch
	cfg, err := h.stores.Providers.GetByID(c.Request.Context(), req.ProviderConfigID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "provider config not found"})
		return
	}

	added, updated, fetched, err := h.fetchModelsForProvider(c, cfg)
	if err != nil {
		if errors.Is(err, ErrUpstreamTimeout) {
			c.JSON(http.StatusGatewayTimeout, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusBadGateway, gin.H{"error": "failed to fetch models: " + err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"message": "models synced",
		"added":   added,
		"updated": updated,
		"total":   fetched,
	})
}

// fetchModelsForProvider fetches and syncs models for a single provider config.
func (h *AdminHandler) fetchModelsForProvider(c *gin.Context, cfg *models.ProviderConfig) (added, updated, total int, err error) {
	// Decrypt the API key for the provider API call
	apiKey := ""
	if len(cfg.APIKeyEnc) > 0 {
		if h.vault != nil {
			apiKey, err = h.vault.Decrypt(cfg.APIKeyEnc, cfg.KeyNonce, cfg.KeyScope, "")
			if err != nil {
				return 0, 0, 0, fmt.Errorf("failed to decrypt API key: %w", err)
			}
		} else {
			// No vault — key stored as raw bytes (unencrypted fallback)
			apiKey = string(cfg.APIKeyEnc)
		}
	}

	result, err := syncProviderModels(c.Request.Context(), h.stores, cfg, apiKey)
	if err != nil {
		return 0, 0, 0, err
	}

	h.auditLog(c, "models.fetch", "provider_config", cfg.ID, gin.H{
		"added": result.Added, "updated": result.Updated, "total": result.Total,
	})

	return result.Added, result.Updated, result.Total, nil
}

func (h *AdminHandler) UpdateModelConfig(c *gin.Context) {
	id := c.Param("id")
	var req struct {
		Visibility *string `json:"visibility"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	if req.Visibility != nil {
		if err := h.stores.Catalog.SetVisibility(c.Request.Context(), id, *req.Visibility); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update visibility"})
			return
		}
	}

	c.JSON(http.StatusOK, gin.H{"message": "model updated"})
}

func (h *AdminHandler) BulkUpdateModels(c *gin.Context) {
	var req struct {
		ProviderConfigID string `json:"provider_config_id"`
		Visibility       string `json:"visibility" binding:"required"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	var err error
	if req.ProviderConfigID != "" {
		err = h.stores.Catalog.BulkSetVisibility(c.Request.Context(), req.ProviderConfigID, req.Visibility)
	} else {
		err = h.stores.Catalog.BulkSetVisibilityAll(c.Request.Context(), req.Visibility)
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to bulk update"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "bulk update complete"})
}

func (h *AdminHandler) DeleteModelConfig(c *gin.Context) {
	id := c.Param("id")
	if err := h.stores.Catalog.Delete(c.Request.Context(), id); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete model"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"message": "model deleted"})
}

// ── Stats ───────────────────────────────────

func (h *AdminHandler) GetStats(c *gin.Context) {
	ctx := c.Request.Context()
	stats := gin.H{}

	userCount, _ := h.stores.Users.CountAll(ctx)
	channelCount, _ := h.stores.Channels.CountAll(ctx)
	messageCount, _ := h.stores.Messages.CountAll(ctx)

	stats["users"] = userCount
	stats["channels"] = channelCount
	stats["messages"] = messageCount

	c.JSON(http.StatusOK, stats)
}

// ── Helpers ─────────────────────────────────
// NOTE: ListAuditLog and ListAuditActions are in audit.go

func (h *AdminHandler) auditLog(c *gin.Context, action, resourceType, resourceID string, metadata interface{}) {
	// Convert interface{} metadata to map[string]interface{} for the shared helper
	var meta map[string]interface{}
	if metadata != nil {
		b, _ := json.Marshal(metadata)
		json.Unmarshal(b, &meta)
	}
	AuditLog(h.stores.Audit, c, action, resourceType, resourceID, meta)
}

// ── Archived Channels (v0.23.2) ──────────────

func (h *AdminHandler) ListArchivedChannels(c *gin.Context) {
	page, perPage, offset := parsePagination(c)

	channels, total, err := h.stores.Channels.ListArchived(c.Request.Context(), store.ListOptions{
		Limit:  perPage,
		Offset: offset,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
		return
	}

	// Retention config
	retentionTTL := 0
	if ttlCfg, err := h.stores.GlobalConfig.Get(c.Request.Context(), "retention_ttl_days"); err == nil {
		if v, ok := ttlCfg["value"].(float64); ok {
			retentionTTL = int(v)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"data":               channels,
		"total":              total,
		"page":               page,
		"per_page":           perPage,
		"retention_ttl_days": retentionTTL,
	})
}

func (h *AdminHandler) PurgeChannel(c *gin.Context) {
	channelID := c.Param("id")

	// Clean up file storage blobs before CASCADE deletes the DB references
	if h.objStore != nil {
		prefix := fmt.Sprintf("files/%s", channelID)
		if err := h.objStore.DeletePrefix(c.Request.Context(), prefix); err != nil {
			log.Printf("⚠️ storage cleanup for purged channel %s failed: %v", channelID, err)
		}
	}

	if err := h.stores.Channels.Purge(c.Request.Context(), channelID); err != nil {
		msg := err.Error()
		switch {
		case msg == "channel not found":
			c.JSON(http.StatusNotFound, gin.H{"error": msg})
		case msg == "channel must be archived before purging":
			c.JSON(http.StatusConflict, gin.H{"error": msg})
		default:
			c.JSON(http.StatusInternalServerError, gin.H{"error": "purge failed"})
		}
		return
	}

	h.auditLog(c, "channel.purged", "channel", channelID, nil)
	c.JSON(http.StatusOK, gin.H{"ok": true})
}
