package handlers

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

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

	"switchboard-core/auth"
	"switchboard-core/crypto"
	"switchboard-core/database"
	"switchboard-core/models"
	"switchboard-core/storage"
	"switchboard-core/store"
)

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"`
		IsAdmin  bool   `json:"is_admin"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx := c.Request.Context()
	hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcryptCost)

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

	if err := h.stores.Users.Create(ctx, 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
	}

	auth.EnsureEveryoneGroup(ctx, h.stores, user.ID)
	if req.IsAdmin {
		auth.AddToAdminsGroup(ctx, h.stores, user.ID)
	}

	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")
	ctx := c.Request.Context()
	var req struct {
		IsAdmin bool `json:"is_admin"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	if _, err := h.stores.Users.GetByID(ctx, id); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
		return
	}

	if !req.IsAdmin {
		// Guard: prevent removing the last admin from Admins group.
		members, _ := h.stores.Groups.ListMembers(ctx, auth.AdminsGroupID)
		isMember := false
		for _, m := range members {
			if m.UserID == id {
				isMember = true
				break
			}
		}
		if isMember && len(members) <= 1 {
			c.JSON(http.StatusConflict, gin.H{"error": "cannot remove the last admin"})
			return
		}
		auth.RemoveFromAdminsGroup(ctx, h.stores, id)
	} else {
		auth.AddToAdminsGroup(ctx, h.stores, id)
	}

	h.auditLog(c, "user.role_change", "user", id, gin.H{"is_admin": req.IsAdmin})
	if h.onUserChanged != nil {
		h.onUserChanged(id)
	}
	c.JSON(http.StatusOK, gin.H{"message": "admin status 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
	if _, err := h.stores.Users.GetByID(c.Request.Context(), id); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
		return
	}
	members, _ := h.stores.Groups.ListMembers(c.Request.Context(), auth.AdminsGroupID)
	isAdmin := false
	for _, m := range members {
		if m.UserID == id {
			isAdmin = true
			break
		}
	}
	if isAdmin && len(members) <= 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); 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)

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


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")
	policies, _ := h.stores.Policies.GetAll(c.Request.Context())

	c.JSON(http.StatusOK, gin.H{
		"banner":             banner,
		"branding":           branding,
		"storage_configured": storageConfigured,
		"policies": gin.H{
			"allow_registration": policies["allow_registration"],
		},
	})
}

// ── 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)
}

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

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

	userCount, _ := h.stores.Users.CountAll(ctx)
	stats["users"] = userCount

	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)
}
