package admin

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"math"
	"strconv"
	"strings"
	"time"

	"github.com/Wei-Shaw/sub2api/internal/handler/dto"
	"github.com/Wei-Shaw/sub2api/internal/handler/quotaview"
	"github.com/Wei-Shaw/sub2api/internal/pkg/response"
	"github.com/Wei-Shaw/sub2api/internal/server/middleware"
	"github.com/Wei-Shaw/sub2api/internal/service"

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

// UserWithConcurrency wraps AdminUser with current concurrency info
type UserWithConcurrency struct {
	dto.AdminUser
	CurrentConcurrency int `json:"current_concurrency"`
}

// UserHandler handles admin user management
type UserHandler struct {
	adminService          service.AdminService
	concurrencyService    *service.ConcurrencyService
	userPlatformQuotaRepo service.UserPlatformQuotaRepository // T13 admin quota view
	billingCache          service.BillingCache                // T17/T18 缓存失效（PUT/POST 路径）
	totpService           *service.TotpService                // 角色提升为管理员的 step-up 门控
	userService           *service.UserService
	settingService        *service.SettingService // step-up 功能开关
}

// NewUserHandler creates a new admin user handler
func NewUserHandler(
	adminService service.AdminService,
	concurrencyService *service.ConcurrencyService,
	userPlatformQuotaRepo service.UserPlatformQuotaRepository,
	billingCache service.BillingCache,
	totpService *service.TotpService,
	userService *service.UserService,
	settingService *service.SettingService,
) *UserHandler {
	return &UserHandler{
		adminService:          adminService,
		concurrencyService:    concurrencyService,
		userPlatformQuotaRepo: userPlatformQuotaRepo,
		billingCache:          billingCache,
		totpService:           totpService,
		userService:           userService,
		settingService:        settingService,
	}
}

// CreateUserRequest represents admin create user request
type CreateUserRequest struct {
	Email         string   `json:"email" binding:"required,email"`
	Password      string   `json:"password" binding:"required,min=6"`
	Username      string   `json:"username"`
	Notes         string   `json:"notes"`
	Role          string   `json:"role" binding:"omitempty,oneof=admin user"`
	Balance       *float64 `json:"balance"`
	Concurrency   int      `json:"concurrency"`
	RPMLimit      int      `json:"rpm_limit"`
	AllowedGroups []int64  `json:"allowed_groups"`
}

// UpdateUserRequest represents admin update user request
// 使用指针类型来区分"未提供"和"设置为0"
type UpdateUserRequest struct {
	Email         string   `json:"email" binding:"omitempty,email"`
	Password      string   `json:"password" binding:"omitempty,min=6"`
	Username      *string  `json:"username"`
	Notes         *string  `json:"notes"`
	Role          string   `json:"role" binding:"omitempty,oneof=admin user"`
	Balance       *float64 `json:"balance"`
	Concurrency   *int     `json:"concurrency"`
	RPMLimit      *int     `json:"rpm_limit"`
	Status        string   `json:"status" binding:"omitempty,oneof=active disabled"`
	AllowedGroups *[]int64 `json:"allowed_groups"`
	// GroupRates 用户专属分组倍率配置
	// map[groupID]*rate，nil 表示删除该分组的专属倍率
	GroupRates map[int64]*float64 `json:"group_rates"`
}

// UpdateBalanceRequest represents balance update request
type UpdateBalanceRequest struct {
	Balance   float64 `json:"balance" binding:"required,gt=0"`
	Operation string  `json:"operation" binding:"required,oneof=set add subtract"`
	Notes     string  `json:"notes"`
}

type BindUserAuthIdentityRequest struct {
	ProviderType    string                              `json:"provider_type"`
	ProviderKey     string                              `json:"provider_key"`
	ProviderSubject string                              `json:"provider_subject"`
	Issuer          *string                             `json:"issuer"`
	Metadata        map[string]any                      `json:"metadata"`
	Channel         *BindUserAuthIdentityChannelRequest `json:"channel"`
}

type BindUserAuthIdentityChannelRequest struct {
	Channel        string         `json:"channel"`
	ChannelAppID   string         `json:"channel_app_id"`
	ChannelSubject string         `json:"channel_subject"`
	Metadata       map[string]any `json:"metadata"`
}

// List handles listing all users with pagination
// GET /api/v1/admin/users
// Query params:
//   - status: filter by user status
//   - role: filter by user role
//   - search: search in email, username
//   - attr[{id}]: filter by custom attribute value, e.g. attr[1]=company
//   - group_name: fuzzy filter by allowed group name
//   - api_key_group_id: filter by the exact group bound to the user's API keys
func (h *UserHandler) List(c *gin.Context) {
	page, pageSize := response.ParsePagination(c)

	search := c.Query("search")
	// 标准化和验证 search 参数
	search = strings.TrimSpace(search)
	if runes := []rune(search); len(runes) > 100 {
		search = string(runes[:100])
	}

	filters := service.UserListFilters{
		Status:     c.Query("status"),
		Role:       c.Query("role"),
		Search:     search,
		GroupName:  strings.TrimSpace(c.Query("group_name")),
		Attributes: parseAttributeFilters(c),
	}
	if raw := strings.TrimSpace(c.Query("api_key_group_id")); raw != "" {
		if id, parseErr := strconv.ParseInt(raw, 10, 64); parseErr == nil && id > 0 {
			filters.APIKeyGroupID = id
		}
	}
	sortBy := c.DefaultQuery("sort_by", "created_at")
	sortOrder := c.DefaultQuery("sort_order", "desc")
	if raw, ok := c.GetQuery("include_subscriptions"); ok {
		includeSubscriptions := parseBoolQueryWithDefault(raw, true)
		filters.IncludeSubscriptions = &includeSubscriptions
	}

	users, total, err := h.adminService.ListUsers(c.Request.Context(), page, pageSize, filters, sortBy, sortOrder)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	// Batch get current concurrency (nil map if unavailable)
	var loadInfo map[int64]*service.UserLoadInfo
	if len(users) > 0 && h.concurrencyService != nil {
		usersConcurrency := make([]service.UserWithConcurrency, len(users))
		for i := range users {
			usersConcurrency[i] = service.UserWithConcurrency{
				ID:             users[i].ID,
				MaxConcurrency: users[i].Concurrency,
			}
		}
		loadInfo, _ = h.concurrencyService.GetUsersLoadBatch(c.Request.Context(), usersConcurrency)
	}

	// Build response with concurrency info
	out := make([]UserWithConcurrency, len(users))
	for i := range users {
		out[i] = UserWithConcurrency{
			AdminUser: *dto.UserFromServiceAdmin(&users[i]),
		}
		if info := loadInfo[users[i].ID]; info != nil {
			out[i].CurrentConcurrency = info.CurrentConcurrency
		}
	}

	response.Paginated(c, out, total, page, pageSize)
}

// parseAttributeFilters extracts attribute filters from query params
// Format: attr[{attributeID}]=value, e.g. attr[1]=company&attr[2]=developer
func parseAttributeFilters(c *gin.Context) map[int64]string {
	result := make(map[int64]string)

	// Get all query params and look for attr[*] pattern
	for key, values := range c.Request.URL.Query() {
		if len(values) == 0 || values[0] == "" {
			continue
		}
		// Check if key matches pattern attr[{id}]
		if len(key) > 5 && key[:5] == "attr[" && key[len(key)-1] == ']' {
			idStr := key[5 : len(key)-1]
			id, err := strconv.ParseInt(idStr, 10, 64)
			if err == nil && id > 0 {
				result[id] = values[0]
			}
		}
	}

	return result
}

// GetByID handles getting a user by ID
// GET /api/v1/admin/users/:id
func (h *UserHandler) GetByID(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	var user *service.User
	if c.Query("include_deleted") == "true" {
		user, err = h.adminService.GetUserIncludeDeleted(c.Request.Context(), userID)
	} else {
		user, err = h.adminService.GetUser(c.Request.Context(), userID)
	}
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, dto.UserFromServiceAdmin(user))
}

// BindAuthIdentity manually binds a canonical auth identity to a user.
// POST /api/v1/admin/users/:id/auth-identities
func (h *UserHandler) BindAuthIdentity(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	var req BindUserAuthIdentityRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}

	input := service.AdminBindAuthIdentityInput{
		ProviderType:    req.ProviderType,
		ProviderKey:     req.ProviderKey,
		ProviderSubject: req.ProviderSubject,
		Issuer:          req.Issuer,
		Metadata:        req.Metadata,
	}
	if req.Channel != nil {
		input.Channel = &service.AdminBindAuthIdentityChannelInput{
			Channel:        req.Channel.Channel,
			ChannelAppID:   req.Channel.ChannelAppID,
			ChannelSubject: req.Channel.ChannelSubject,
			Metadata:       req.Channel.Metadata,
		}
	}

	result, err := h.adminService.BindUserAuthIdentity(c.Request.Context(), userID, input)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	response.Success(c, result)
}

// Create handles creating a new user
// POST /api/v1/admin/users
func (h *UserHandler) Create(c *gin.Context) {
	var req CreateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}

	// 创建管理员账号属权限敏感操作：需最近完成 step-up 2FA 验证。
	if req.Role == service.RoleAdmin {
		if !middleware.EnforceStepUp(c, h.totpService, h.userService, h.settingService) {
			return
		}
	}

	user, err := h.adminService.CreateUser(c.Request.Context(), &service.CreateUserInput{
		Email:         req.Email,
		Password:      req.Password,
		Username:      req.Username,
		Notes:         req.Notes,
		Role:          req.Role,
		Balance:       req.Balance,
		Concurrency:   req.Concurrency,
		RPMLimit:      req.RPMLimit,
		AllowedGroups: req.AllowedGroups,
		ActorAdminID:  getAdminIDFromContext(c),
	})
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, dto.UserFromServiceAdmin(user))
}

// Update handles updating a user
// PUT /api/v1/admin/users/:id
func (h *UserHandler) Update(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	var req UpdateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}

	// 防锁死保护：管理员不能把自己降级为普通用户(单管理员场景下会失去后台访问权)。
	// 与既有"不能禁用/删除 admin"保护一致。降级其他管理员仍然允许。
	if req.Role == service.RoleUser && userID == getAdminIDFromContext(c) {
		response.BadRequest(c, "cannot demote yourself from admin")
		return
	}

	// 把普通用户提升为管理员属权限敏感操作：需最近完成 step-up 2FA 验证。
	// 目标已是管理员时（前端编辑表单总是携带 role）不触发，避免日常编辑被打断。
	if req.Role == service.RoleAdmin {
		target, err := h.adminService.GetUser(c.Request.Context(), userID)
		if err != nil {
			response.ErrorFrom(c, err)
			return
		}
		if target.Role != service.RoleAdmin {
			if !middleware.EnforceStepUp(c, h.totpService, h.userService, h.settingService) {
				return
			}
		}
	}

	// 使用指针类型直接传递，nil 表示未提供该字段
	user, err := h.adminService.UpdateUser(c.Request.Context(), userID, &service.UpdateUserInput{
		Email:         req.Email,
		Password:      req.Password,
		Username:      req.Username,
		Notes:         req.Notes,
		Role:          req.Role,
		Balance:       req.Balance,
		Concurrency:   req.Concurrency,
		RPMLimit:      req.RPMLimit,
		Status:        req.Status,
		AllowedGroups: req.AllowedGroups,
		GroupRates:    req.GroupRates,
		ActorAdminID:  getAdminIDFromContext(c),
	})
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, dto.UserFromServiceAdmin(user))
}

// Delete handles deleting a user
// DELETE /api/v1/admin/users/:id
func (h *UserHandler) Delete(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	err = h.adminService.DeleteUser(c.Request.Context(), userID)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, gin.H{"message": "User deleted successfully"})
}

// UpdateBalance handles updating user balance
// POST /api/v1/admin/users/:id/balance
func (h *UserHandler) UpdateBalance(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	var req UpdateBalanceRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}

	idempotencyPayload := struct {
		UserID int64                `json:"user_id"`
		Body   UpdateBalanceRequest `json:"body"`
	}{
		UserID: userID,
		Body:   req,
	}
	executeAdminIdempotentJSON(c, "admin.users.balance.update", idempotencyPayload, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
		user, execErr := h.adminService.UpdateUserBalance(ctx, userID, req.Balance, req.Operation, req.Notes)
		if execErr != nil {
			return nil, execErr
		}
		return dto.UserFromServiceAdmin(user), nil
	})
}

// GetUserAPIKeys handles getting user's API keys
// GET /api/v1/admin/users/:id/api-keys
func (h *UserHandler) GetUserAPIKeys(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	page, pageSize := response.ParsePagination(c)
	sortBy := c.DefaultQuery("sort_by", "created_at")
	sortOrder := c.DefaultQuery("sort_order", "desc")

	keys, total, err := h.adminService.GetUserAPIKeys(c.Request.Context(), userID, page, pageSize, sortBy, sortOrder)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	out := make([]dto.APIKey, 0, len(keys))
	for i := range keys {
		out = append(out, *dto.APIKeyFromService(&keys[i]))
	}
	response.Paginated(c, out, total, page, pageSize)
}

// GetUserUsage handles getting user's usage statistics
// GET /api/v1/admin/users/:id/usage
func (h *UserHandler) GetUserUsage(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	period := c.DefaultQuery("period", "month")

	stats, err := h.adminService.GetUserUsageStats(c.Request.Context(), userID, period)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, stats)
}

// GetBalanceHistory handles getting user's balance/concurrency change history
// GET /api/v1/admin/users/:id/balance-history
// Query params:
//   - type: filter by record type (balance, affiliate_balance, admin_balance, concurrency, admin_concurrency, subscription)
func (h *UserHandler) GetBalanceHistory(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	page, pageSize := response.ParsePagination(c)
	codeType := c.Query("type")

	codes, total, totalRecharged, err := h.adminService.GetUserBalanceHistory(c.Request.Context(), userID, page, pageSize, codeType)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	// Convert to admin DTO (includes notes field for admin visibility)
	out := make([]dto.AdminRedeemCode, 0, len(codes))
	for i := range codes {
		out = append(out, *dto.RedeemCodeFromServiceAdmin(&codes[i]))
	}

	// Custom response with total_recharged alongside pagination
	pages := int((total + int64(pageSize) - 1) / int64(pageSize))
	if pages < 1 {
		pages = 1
	}
	response.Success(c, gin.H{
		"items":           out,
		"total":           total,
		"page":            page,
		"page_size":       pageSize,
		"pages":           pages,
		"total_recharged": totalRecharged,
	})
}

// ReplaceGroupRequest represents the request to replace a user's exclusive group
type ReplaceGroupRequest struct {
	OldGroupID int64 `json:"old_group_id" binding:"required,gt=0"`
	NewGroupID int64 `json:"new_group_id" binding:"required,gt=0"`
}

// ReplaceGroup handles replacing a user's exclusive group
// POST /api/v1/admin/users/:id/replace-group
func (h *UserHandler) ReplaceGroup(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	var req ReplaceGroupRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}

	result, err := h.adminService.ReplaceUserGroup(c.Request.Context(), userID, req.OldGroupID, req.NewGroupID)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, gin.H{
		"migrated_keys": result.MigratedKeys,
	})
}

// GetUserRPMStatus 返回指定用户当前分钟的 RPM 用量
// GET /api/v1/admin/users/:id/rpm-status
func (h *UserHandler) GetUserRPMStatus(c *gin.Context) {
	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	status, err := h.adminService.GetUserRPMStatus(c.Request.Context(), userID)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, status)
}

// BatchUpdateConcurrency 批量修改用户并发数
// POST /api/v1/admin/users/batch-concurrency
type BatchUpdateConcurrencyRequest struct {
	UserIDs     []int64 `json:"user_ids"`
	All         bool    `json:"all"`
	Concurrency int     `json:"concurrency"`
	Mode        string  `json:"mode" binding:"required,oneof=set add"`
}

func (h *UserHandler) BatchUpdateConcurrency(c *gin.Context) {
	var req BatchUpdateConcurrencyRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}
	if !req.All && len(req.UserIDs) == 0 {
		response.BadRequest(c, "user_ids is required unless all=true")
		return
	}
	if len(req.UserIDs) > 500 {
		response.BadRequest(c, "user_ids cannot exceed 500")
		return
	}

	var userIDs []int64
	if req.All {
		// Fetch all user IDs via pagination
		page := 1
		const pageSize = 500
		for {
			users, _, err := h.adminService.ListUsers(c.Request.Context(), page, pageSize, service.UserListFilters{}, "id", "asc")
			if err != nil {
				response.ErrorFrom(c, err)
				return
			}
			for _, u := range users {
				userIDs = append(userIDs, u.ID)
			}
			if len(users) < pageSize {
				break
			}
			page++
		}
	} else {
		userIDs = req.UserIDs
	}

	if len(userIDs) == 0 {
		response.Success(c, gin.H{"affected": 0})
		return
	}

	affected, err := h.adminService.BatchUpdateConcurrency(c.Request.Context(), userIDs, req.Concurrency, req.Mode)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	response.Success(c, gin.H{"affected": affected})
}

// BatchUpdateLimits overwrites concurrency and/or RPM limits for multiple users.
// POST /api/v1/admin/users/batch-limits
type BatchUpdateLimitsRequest struct {
	UserIDs     []int64 `json:"user_ids"`
	All         bool    `json:"all"`
	Concurrency *int    `json:"concurrency" binding:"omitempty,min=0"`
	RPMLimit    *int    `json:"rpm_limit" binding:"omitempty,min=0"`
}

func (h *UserHandler) BatchUpdateLimits(c *gin.Context) {
	var req BatchUpdateLimitsRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}
	if req.Concurrency == nil && req.RPMLimit == nil {
		response.BadRequest(c, "at least one of concurrency or rpm_limit is required")
		return
	}
	if !req.All && len(req.UserIDs) == 0 {
		response.BadRequest(c, "user_ids is required unless all=true")
		return
	}
	if !req.All && len(req.UserIDs) > 500 {
		response.BadRequest(c, "user_ids cannot exceed 500")
		return
	}

	userIDs := req.UserIDs
	if req.All {
		userIDs = nil
		page := 1
		const pageSize = 500
		for {
			users, _, err := h.adminService.ListUsers(c.Request.Context(), page, pageSize, service.UserListFilters{}, "id", "asc")
			if err != nil {
				response.ErrorFrom(c, err)
				return
			}
			for _, user := range users {
				userIDs = append(userIDs, user.ID)
			}
			if len(users) < pageSize {
				break
			}
			page++
		}
	}

	if len(userIDs) == 0 {
		response.Success(c, gin.H{"affected": 0})
		return
	}

	affected, err := h.adminService.BatchUpdateLimits(
		c.Request.Context(),
		userIDs,
		req.Concurrency,
		req.RPMLimit,
	)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	response.Success(c, gin.H{"affected": affected})
}

// GetUserPlatformQuotas GET /admin/users/:id/platform-quotas
// admin 视角：D14 lazy 归零 + 暴露 *_window_start 调试字段
func (h *UserHandler) GetUserPlatformQuotas(c *gin.Context) {
	idStr := c.Param("id")
	userID, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		response.BadRequest(c, "invalid user id")
		return
	}
	if h.userPlatformQuotaRepo == nil {
		response.Success(c, map[string]any{"platform_quotas": []any{}})
		return
	}
	// 校验用户存在：与 PUT/POST 路径一致，不存在返回 404 而非空数组（避免 admin 界面误判用户存在）。
	if _, err := h.adminService.GetUser(c.Request.Context(), userID); err != nil {
		response.ErrorFrom(c, err)
		return
	}
	records, err := h.userPlatformQuotaRepo.ListByUser(c.Request.Context(), userID)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	now := time.Now().UTC()
	out := make([]map[string]any, 0, len(records))
	for _, r := range records {
		out = append(out, quotaview.LazyZeroQuotaForResponse(r, now, true)) // true = 暴露 window_start
	}
	response.Success(c, map[string]any{"platform_quotas": out})
}

// UpdateUserPlatformQuotasRequest is the body for PUT /admin/users/:id/platform-quotas.
type UpdateUserPlatformQuotasRequest struct {
	Quotas []PlatformQuotaInput `json:"quotas" binding:"required"`
}

// PlatformQuotaInput 单平台限额输入；limit 字段为 nil 表示不限制。
type PlatformQuotaInput struct {
	Platform        string   `json:"platform" binding:"required"`
	DailyLimitUSD   *float64 `json:"daily_limit_usd"`
	WeeklyLimitUSD  *float64 `json:"weekly_limit_usd"`
	MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
}

// platform 合法性由 service.IsAllowedQuotaPlatform / service.AllowedQuotaPlatforms 统一判断（单一源）。

// UpdateUserPlatformQuotas PUT /admin/users/:id/platform-quotas
// 全量替换该用户所有平台限额。
func (h *UserHandler) UpdateUserPlatformQuotas(c *gin.Context) {
	if h.userPlatformQuotaRepo == nil {
		response.Error(c, 503, "platform quota service not available")
		return
	}

	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	var req UpdateUserPlatformQuotasRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}

	if len(req.Quotas) > len(service.AllowedQuotaPlatforms) {
		response.BadRequest(c, fmt.Sprintf("quotas length must be <= %d", len(service.AllowedQuotaPlatforms)))
		return
	}
	seen := make(map[string]struct{}, len(req.Quotas))
	for _, q := range req.Quotas {
		if !service.IsAllowedQuotaPlatform(q.Platform) {
			response.BadRequest(c, "invalid platform: "+q.Platform)
			return
		}
		if _, dup := seen[q.Platform]; dup {
			response.BadRequest(c, "duplicate platform: "+q.Platform)
			return
		}
		seen[q.Platform] = struct{}{}
		// daily_limit_usd / weekly_limit_usd / monthly_limit_usd 的语义：
		//   nil / not set → 无限额（完全放行）
		//   0            → 完全禁用（任何请求都会被拒绝，因为 usage >= 0 恒成立）
		//   > 0          → USD 限额上限
		// 拦截 NaN / ±Inf：客户端可发送超大数（如 1e308 × 2）使 JSON 反序列化得到 +Inf，
		// 进入 DB 后 cache check 中 usage >= limit 永不成立，limit 等同失效。
		for _, f := range []struct {
			name string
			val  *float64
		}{
			{"daily_limit_usd", q.DailyLimitUSD},
			{"weekly_limit_usd", q.WeeklyLimitUSD},
			{"monthly_limit_usd", q.MonthlyLimitUSD},
		} {
			if f.val == nil {
				continue
			}
			v := *f.val
			if v < 0 {
				response.BadRequest(c, f.name+" must be >= 0")
				return
			}
			if math.IsNaN(v) || math.IsInf(v, 0) {
				response.BadRequest(c, f.name+" must be a finite number")
				return
			}
		}
	}

	records := make([]service.UserPlatformQuotaRecord, 0, len(req.Quotas))
	for _, q := range req.Quotas {
		records = append(records, service.UserPlatformQuotaRecord{
			UserID:          userID,
			Platform:        q.Platform,
			DailyLimitUSD:   q.DailyLimitUSD,
			WeeklyLimitUSD:  q.WeeklyLimitUSD,
			MonthlyLimitUSD: q.MonthlyLimitUSD,
		})
	}

	ctx := c.Request.Context()
	// 校验用户是否存在，避免 FK 违反导致 500；用户不存在时返回 404。
	if _, err := h.adminService.GetUser(ctx, userID); err != nil {
		response.ErrorFrom(c, err)
		return
	}
	// 在 UpsertForUser 之前抓取 before snapshot 用于审计 before/after 对比。
	// ListByUser 失败不阻断主操作（best-effort），仅记录降级 warn。
	beforeRecords, beforeErr := h.userPlatformQuotaRepo.ListByUser(ctx, userID)
	if beforeErr != nil {
		slog.Warn("quota audit before snapshot failed", "user_id", userID, "err", beforeErr)
	}
	if err := h.userPlatformQuotaRepo.UpsertForUser(ctx, userID, records); err != nil {
		response.ErrorFrom(c, err)
		return
	}

	beforeByPlatform := make(map[string]service.UserPlatformQuotaRecord, len(beforeRecords))
	for _, r := range beforeRecords {
		beforeByPlatform[r.Platform] = r
	}
	afterPlatforms := make(map[string]struct{}, len(records))
	for _, r := range records {
		afterPlatforms[r.Platform] = struct{}{}
	}
	changes := make([]map[string]any, 0, len(records))
	for _, r := range records {
		entry := map[string]any{
			"platform":          r.Platform,
			"daily_limit_usd":   r.DailyLimitUSD,
			"weekly_limit_usd":  r.WeeklyLimitUSD,
			"monthly_limit_usd": r.MonthlyLimitUSD,
		}
		if prev, ok := beforeByPlatform[r.Platform]; ok {
			entry["before_daily_limit_usd"] = prev.DailyLimitUSD
			entry["before_weekly_limit_usd"] = prev.WeeklyLimitUSD
			entry["before_monthly_limit_usd"] = prev.MonthlyLimitUSD
		}
		changes = append(changes, entry)
	}
	// 补 removed 条目：before 存在但 after 缺失 = 该平台被软删除。
	// 缺少这条记录，审计消费方无法察觉"管理员把某平台从配额列表移除"的操作（合规盲区）。
	for _, prev := range beforeRecords {
		if _, kept := afterPlatforms[prev.Platform]; kept {
			continue
		}
		changes = append(changes, map[string]any{
			"platform":                 prev.Platform,
			"removed":                  true,
			"before_daily_limit_usd":   prev.DailyLimitUSD,
			"before_weekly_limit_usd":  prev.WeeklyLimitUSD,
			"before_monthly_limit_usd": prev.MonthlyLimitUSD,
		})
	}
	// before_snapshot_available 让审计消费方能识别 changes 中是否带 before_* 字段；
	// false 时所有 entry 都会缺失 before_*_limit_usd，仅有 after 视图。
	slog.Info("admin.quota_updated",
		"actor_admin_id", getAdminIDFromContext(c),
		"target_user_id", userID,
		"platform_count", len(records),
		"before_snapshot_available", beforeErr == nil,
		"changes", changes)

	// 失效 cache：对全部允许的 platform 统一 invalidate。
	// Trade-off：精确失效（仅 req 涉及平台 + 被软删平台）需 upsert 前额外 ListByUser，
	// 增加一次 DB 查询和逻辑复杂度。由于 AllowedQuotaPlatforms 数量很少，
	// 全量 invalidate 的额外开销可接受，且能可靠覆盖软删除场景。
	if h.billingCache != nil {
		for _, p := range service.AllowedQuotaPlatforms {
			if err := h.billingCache.DeleteUserPlatformQuotaCache(ctx, userID, p); err != nil {
				slog.Error("ALERT: quota cache invalidation failed after UpsertForUser; limit 生效可能延迟至 sentinel TTL(最长 1h),需人工确认或重试失效", "user_id", userID, "platform", p, "err", err)
			}
		}
	}

	// 返回最新状态
	now := time.Now().UTC()
	records2, err := h.userPlatformQuotaRepo.ListByUser(ctx, userID)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	out := make([]map[string]any, 0, len(records2))
	for i := range records2 {
		out = append(out, quotaview.LazyZeroQuotaForResponse(records2[i], now, true))
	}
	response.Success(c, map[string]any{"platform_quotas": out})
}

// ResetUserPlatformQuotaWindowRequest is the body for POST /admin/users/:id/platform-quotas/reset.
type ResetUserPlatformQuotaWindowRequest struct {
	Platform string `json:"platform" binding:"required"`
	Window   string `json:"window" binding:"required"`
}

var allowedWindowsForQuotaReset = map[string]struct{}{
	"daily":   {},
	"weekly":  {},
	"monthly": {},
}

// ResetUserPlatformQuotaWindow POST /admin/users/:id/platform-quotas/reset
// 立即归零指定 (platform, window) 的用量并更新 window_start。
func (h *UserHandler) ResetUserPlatformQuotaWindow(c *gin.Context) {
	if h.userPlatformQuotaRepo == nil {
		response.Error(c, 503, "platform quota service not available")
		return
	}

	userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid user ID")
		return
	}

	var req ResetUserPlatformQuotaWindowRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}

	if !service.IsAllowedQuotaPlatform(req.Platform) {
		response.BadRequest(c, "invalid platform: "+req.Platform)
		return
	}
	if _, ok := allowedWindowsForQuotaReset[req.Window]; !ok {
		response.BadRequest(c, "invalid window: "+req.Window)
		return
	}

	ctx := c.Request.Context()
	// 校验用户是否存在，避免对不存在的用户执行操作返回误导性的 500。
	if _, err := h.adminService.GetUser(ctx, userID); err != nil {
		response.ErrorFrom(c, err)
		return
	}
	now := time.Now().UTC()
	if err := h.userPlatformQuotaRepo.ResetExpiredWindow(ctx, userID, req.Platform, req.Window, now); err != nil {
		if errors.Is(err, service.ErrUserPlatformQuotaNotFound) {
			response.NotFound(c, "user platform quota not found")
			return
		}
		response.ErrorFrom(c, err)
		return
	}

	slog.Info("admin.quota_window_reset",
		"actor_admin_id", getAdminIDFromContext(c),
		"target_user_id", userID,
		"platform", req.Platform,
		"window", req.Window)

	if h.billingCache != nil {
		if err := h.billingCache.DeleteUserPlatformQuotaCache(ctx, userID, req.Platform); err != nil {
			slog.Error("ALERT: quota cache invalidation failed after ResetExpiredWindow; 窗口重置可能延迟至 sentinel TTL(最长 1h)", "user_id", userID, "platform", req.Platform, "err", err)
		}
	}

	records, err := h.userPlatformQuotaRepo.ListByUser(ctx, userID)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	out := make([]map[string]any, 0, len(records))
	for i := range records {
		out = append(out, quotaview.LazyZeroQuotaForResponse(records[i], now, true))
	}
	response.Success(c, map[string]any{"platform_quotas": out})
}
