package admin

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"strconv"
	"strings"
	"time"

	"github.com/Wei-Shaw/sub2api/internal/handler/dto"
	infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
	"github.com/Wei-Shaw/sub2api/internal/pkg/response"
	"github.com/Wei-Shaw/sub2api/internal/platform/liveattestation"
	"github.com/Wei-Shaw/sub2api/internal/service"

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

// GroupHandler handles admin group management
type GroupHandler struct {
	adminService         service.AdminService
	dashboardService     *service.DashboardService
	groupCapacityService *service.GroupCapacityService
}

// GetLiveCapability 返回当前服务端是否具备生成 Live attestation 的运行环境。
func (h *GroupHandler) GetLiveCapability(c *gin.Context) {
	err := liveattestation.NewProvider().Check(c.Request.Context())
	result := gin.H{"supported": err == nil}
	if err != nil {
		result["reason"] = err.Error()
	}
	response.Success(c, result)
}

type optionalLimitField struct {
	set   bool
	value *float64
}

func (f *optionalLimitField) UnmarshalJSON(data []byte) error {
	f.set = true

	trimmed := bytes.TrimSpace(data)
	if bytes.Equal(trimmed, []byte("null")) {
		f.value = nil
		return nil
	}

	var number float64
	if err := json.Unmarshal(trimmed, &number); err == nil {
		f.value = &number
		return nil
	}

	var text string
	if err := json.Unmarshal(trimmed, &text); err == nil {
		text = strings.TrimSpace(text)
		if text == "" {
			f.value = nil
			return nil
		}
		number, err = strconv.ParseFloat(text, 64)
		if err != nil {
			return fmt.Errorf("invalid numeric limit value %q: %w", text, err)
		}
		f.value = &number
		return nil
	}

	return fmt.Errorf("invalid limit value: %s", string(trimmed))
}

func (f optionalLimitField) ToServiceInput() *float64 {
	if !f.set {
		return nil
	}
	if f.value != nil {
		return f.value
	}
	zero := 0.0
	return &zero
}

// NewGroupHandler creates a new admin group handler
func NewGroupHandler(adminService service.AdminService, dashboardService *service.DashboardService, groupCapacityService *service.GroupCapacityService) *GroupHandler {
	return &GroupHandler{
		adminService:         adminService,
		dashboardService:     dashboardService,
		groupCapacityService: groupCapacityService,
	}
}

// CreateGroupRequest represents create group request
type CreateGroupRequest struct {
	Name                      string                        `json:"name" binding:"required"`
	Description               string                        `json:"description"`
	Platform                  string                        `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek composite"`
	RateMultiplier            float64                       `json:"rate_multiplier"`
	IsExclusive               bool                          `json:"is_exclusive"`
	SubscriptionType          string                        `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
	DailyLimitUSD             optionalLimitField            `json:"daily_limit_usd"`
	WeeklyLimitUSD            optionalLimitField            `json:"weekly_limit_usd"`
	MonthlyLimitUSD           optionalLimitField            `json:"monthly_limit_usd"`
	LongContextPricingEnabled bool                          `json:"long_context_pricing_enabled"`
	ModelPricing              []service.ChannelModelPricing `json:"model_pricing"`
	// 图片生成计费配置（antigravity 和 gemini 平台使用，负数表示清除配置）
	AllowImageGeneration            bool                          `json:"allow_image_generation"`
	AllowBatchImageGeneration       bool                          `json:"allow_batch_image_generation"`
	ImageRateIndependent            bool                          `json:"image_rate_independent"`
	ImageRateMultiplier             *float64                      `json:"image_rate_multiplier"`
	BatchImageDiscountMultiplier    *float64                      `json:"batch_image_discount_multiplier"`
	BatchImageHoldMultiplier        *float64                      `json:"batch_image_hold_multiplier"`
	VideoRateIndependent            bool                          `json:"video_rate_independent"`
	VideoRateMultiplier             *float64                      `json:"video_rate_multiplier"`
	PeakRateEnabled                 bool                          `json:"peak_rate_enabled"`
	PeakStart                       string                        `json:"peak_start"`
	PeakEnd                         string                        `json:"peak_end"`
	PeakRateMultiplier              *float64                      `json:"peak_rate_multiplier"`
	ProfitControlEnabled            bool                          `json:"profit_control_enabled"`
	ProfitMinMargin                 *float64                      `json:"profit_min_margin"`
	ProfitSafetyBuffer              *float64                      `json:"profit_safety_buffer"`
	ImagePrice1K                    *float64                      `json:"image_price_1k"`
	ImagePrice2K                    *float64                      `json:"image_price_2k"`
	ImagePrice4K                    *float64                      `json:"image_price_4k"`
	VideoPrice480P                  *float64                      `json:"video_price_480p"`
	VideoPrice720P                  *float64                      `json:"video_price_720p"`
	VideoPrice1080P                 *float64                      `json:"video_price_1080p"`
	VideoModelPrices                map[string]map[string]float64 `json:"video_model_prices,omitempty"`
	WebSearchPricePerCall           *float64                      `json:"web_search_price_per_call"`
	SearchPricePer1k                *float64                      `json:"search_price_per_1k"`
	AudioRealtimePricePerMin        *float64                      `json:"audio_realtime_price_per_min"`
	AudioTtsPricePerMillionChars    *float64                      `json:"audio_tts_price_per_million_chars"`
	AudioSttPricePerHour            *float64                      `json:"audio_stt_price_per_hour"`
	ClaudeCodeOnly                  bool                          `json:"claude_code_only"`
	FallbackGroupID                 *int64                        `json:"fallback_group_id"`
	FallbackGroupIDOnInvalidRequest *int64                        `json:"fallback_group_id_on_invalid_request"`
	// 模型路由配置（仅 anthropic 平台使用）
	ModelRouting        map[string][]int64 `json:"model_routing"`
	ModelRoutingEnabled bool               `json:"model_routing_enabled"`
	MCPXMLInject        *bool              `json:"mcp_xml_inject"`
	// 支持的模型系列（仅 antigravity 平台使用）
	SupportedModelScopes []string `json:"supported_model_scopes"`
	// OpenAI Messages 调度配置（仅 openai 平台使用）
	AllowMessagesDispatch       bool                                      `json:"allow_messages_dispatch"`
	AllowLive                   bool                                      `json:"allow_live"`
	RequireOAuthOnly            bool                                      `json:"require_oauth_only"`
	RequirePrivacySet           bool                                      `json:"require_privacy_set"`
	DefaultMappedModel          string                                    `json:"default_mapped_model"`
	MessagesDispatchModelConfig service.OpenAIMessagesDispatchModelConfig `json:"messages_dispatch_model_config"`
	ModelsListConfig            service.GroupModelsListConfig             `json:"models_list_config"`
	// 分组 RPM 上限（0 = 不限制）
	RPMLimit int `json:"rpm_limit"`
	// OpenAI/Codex 请求推理强度上限，空字符串表示不限制。
	MaxReasoningEffort string `json:"max_reasoning_effort"`
	// OpenAI/Codex 推理强度精确映射。
	ReasoningEffortMappings []service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
	// 从指定分组复制账号（创建后自动绑定）
	CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"`
}

// UpdateGroupRequest represents update group request
type UpdateGroupRequest struct {
	Name                      string                         `json:"name"`
	Description               *string                        `json:"description"`
	Platform                  string                         `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek composite"`
	RateMultiplier            *float64                       `json:"rate_multiplier"`
	IsExclusive               *bool                          `json:"is_exclusive"`
	Status                    string                         `json:"status" binding:"omitempty,oneof=active inactive"`
	SubscriptionType          string                         `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
	DailyLimitUSD             optionalLimitField             `json:"daily_limit_usd"`
	WeeklyLimitUSD            optionalLimitField             `json:"weekly_limit_usd"`
	MonthlyLimitUSD           optionalLimitField             `json:"monthly_limit_usd"`
	LongContextPricingEnabled *bool                          `json:"long_context_pricing_enabled"`
	ModelPricing              *[]service.ChannelModelPricing `json:"model_pricing"`
	// 图片生成计费配置（antigravity 和 gemini 平台使用，负数表示清除配置）
	AllowImageGeneration            *bool                         `json:"allow_image_generation"`
	AllowBatchImageGeneration       *bool                         `json:"allow_batch_image_generation"`
	ImageRateIndependent            *bool                         `json:"image_rate_independent"`
	ImageRateMultiplier             *float64                      `json:"image_rate_multiplier"`
	BatchImageDiscountMultiplier    *float64                      `json:"batch_image_discount_multiplier"`
	BatchImageHoldMultiplier        *float64                      `json:"batch_image_hold_multiplier"`
	VideoRateIndependent            *bool                         `json:"video_rate_independent"`
	VideoRateMultiplier             *float64                      `json:"video_rate_multiplier"`
	PeakRateEnabled                 *bool                         `json:"peak_rate_enabled"`
	PeakStart                       *string                       `json:"peak_start"`
	PeakEnd                         *string                       `json:"peak_end"`
	PeakRateMultiplier              *float64                      `json:"peak_rate_multiplier"`
	ProfitControlEnabled            *bool                         `json:"profit_control_enabled"`
	ProfitMinMargin                 *float64                      `json:"profit_min_margin"`
	ProfitSafetyBuffer              *float64                      `json:"profit_safety_buffer"`
	ImagePrice1K                    *float64                      `json:"image_price_1k"`
	ImagePrice2K                    *float64                      `json:"image_price_2k"`
	ImagePrice4K                    *float64                      `json:"image_price_4k"`
	VideoPrice480P                  *float64                      `json:"video_price_480p"`
	VideoPrice720P                  *float64                      `json:"video_price_720p"`
	VideoPrice1080P                 *float64                      `json:"video_price_1080p"`
	VideoModelPrices                map[string]map[string]float64 `json:"video_model_prices,omitempty"`
	WebSearchPricePerCall           *float64                      `json:"web_search_price_per_call"`
	SearchPricePer1k                *float64                      `json:"search_price_per_1k"`
	AudioRealtimePricePerMin        *float64                      `json:"audio_realtime_price_per_min"`
	AudioTtsPricePerMillionChars    *float64                      `json:"audio_tts_price_per_million_chars"`
	AudioSttPricePerHour            *float64                      `json:"audio_stt_price_per_hour"`
	ClaudeCodeOnly                  *bool                         `json:"claude_code_only"`
	FallbackGroupID                 *int64                        `json:"fallback_group_id"`
	FallbackGroupIDOnInvalidRequest *int64                        `json:"fallback_group_id_on_invalid_request"`
	// 模型路由配置（仅 anthropic 平台使用）
	ModelRouting        map[string][]int64 `json:"model_routing"`
	ModelRoutingEnabled *bool              `json:"model_routing_enabled"`
	MCPXMLInject        *bool              `json:"mcp_xml_inject"`
	// 支持的模型系列（仅 antigravity 平台使用）
	SupportedModelScopes *[]string `json:"supported_model_scopes"`
	// OpenAI Messages 调度配置（仅 openai 平台使用）
	AllowMessagesDispatch       *bool                                      `json:"allow_messages_dispatch"`
	AllowLive                   *bool                                      `json:"allow_live"`
	RequireOAuthOnly            *bool                                      `json:"require_oauth_only"`
	RequirePrivacySet           *bool                                      `json:"require_privacy_set"`
	DefaultMappedModel          *string                                    `json:"default_mapped_model"`
	MessagesDispatchModelConfig *service.OpenAIMessagesDispatchModelConfig `json:"messages_dispatch_model_config"`
	ModelsListConfig            *service.GroupModelsListConfig             `json:"models_list_config"`
	// 分组 RPM 上限（0 = 不限制）；nil 表示未提供不改动
	RPMLimit *int `json:"rpm_limit"`
	// OpenAI/Codex 请求推理强度上限；空字符串清除，nil 不修改。
	MaxReasoningEffort *string `json:"max_reasoning_effort"`
	// nil 不修改，空数组清空，非空数组替换。
	ReasoningEffortMappings *[]service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
	// 从指定分组复制账号（同步操作：先清空当前分组的账号绑定，再绑定源分组的账号）
	CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"`
}

type CompositeRouteRequest struct {
	PublicModel    string `json:"public_model" binding:"required"`
	MatchType      string `json:"match_type" binding:"omitempty,oneof=exact prefix"`
	TargetPlatform string `json:"target_platform" binding:"required,oneof=anthropic openai gemini antigravity grok kimi zhipu deepseek"`
	UpstreamModel  string `json:"upstream_model"`
	Endpoint       string `json:"endpoint" binding:"omitempty,oneof=any messages count_tokens responses chat_completions embeddings images gemini"`
	Priority       int    `json:"priority"`
	Enabled        *bool  `json:"enabled"`
	Notes          string `json:"notes"`
}

type CompositeRoutePreviewRequest struct {
	Model    string `json:"model" binding:"required"`
	Endpoint string `json:"endpoint" binding:"omitempty,oneof=any messages count_tokens responses chat_completions embeddings images gemini"`
}

// List handles listing all groups with pagination
// GET /api/v1/admin/groups
func (h *GroupHandler) List(c *gin.Context) {
	page, pageSize := response.ParsePagination(c)
	platform := c.Query("platform")
	status := c.Query("status")
	search := c.Query("search")
	// 标准化和验证 search 参数
	search = strings.TrimSpace(search)
	if len(search) > 100 {
		search = search[:100]
	}
	isExclusiveStr := c.Query("is_exclusive")
	sortBy := c.DefaultQuery("sort_by", "sort_order")
	sortOrder := c.DefaultQuery("sort_order", "asc")

	var isExclusive *bool
	if isExclusiveStr != "" {
		val := isExclusiveStr == "true"
		isExclusive = &val
	}

	groups, total, err := h.adminService.ListGroups(c.Request.Context(), page, pageSize, platform, status, search, isExclusive, sortBy, sortOrder)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	outGroups := make([]dto.AdminGroup, 0, len(groups))
	for i := range groups {
		outGroups = append(outGroups, *dto.GroupFromServiceAdmin(&groups[i]))
	}
	response.Paginated(c, outGroups, total, page, pageSize)
}

// ListCompositeRoutes handles listing composite model routes for one group.
// GET /api/v1/admin/groups/:id/composite-routes
func (h *GroupHandler) ListCompositeRoutes(c *gin.Context) {
	groupID, ok := parsePositiveIDParam(c, "id")
	if !ok {
		return
	}
	routes, err := h.adminService.ListCompositeRoutes(c.Request.Context(), groupID)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	response.Success(c, routes)
}

// CreateCompositeRoute handles creating one composite model route.
// POST /api/v1/admin/groups/:id/composite-routes
func (h *GroupHandler) CreateCompositeRoute(c *gin.Context) {
	groupID, ok := parsePositiveIDParam(c, "id")
	if !ok {
		return
	}
	var req CompositeRouteRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request body: "+err.Error())
		return
	}
	route, err := h.adminService.CreateCompositeRoute(c.Request.Context(), groupID, compositeRouteRequestToInput(req, true))
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	response.Created(c, route)
}

// UpdateCompositeRoute handles replacing one composite model route.
// PUT /api/v1/admin/groups/:id/composite-routes/:route_id
func (h *GroupHandler) UpdateCompositeRoute(c *gin.Context) {
	groupID, ok := parsePositiveIDParam(c, "id")
	if !ok {
		return
	}
	routeID, ok := parsePositiveIDParam(c, "route_id")
	if !ok {
		return
	}
	var req CompositeRouteRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request body: "+err.Error())
		return
	}
	route, err := h.adminService.UpdateCompositeRoute(c.Request.Context(), groupID, routeID, compositeRouteRequestToInput(req, true))
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	response.Success(c, route)
}

// DeleteCompositeRoute handles deleting one composite model route.
// DELETE /api/v1/admin/groups/:id/composite-routes/:route_id
func (h *GroupHandler) DeleteCompositeRoute(c *gin.Context) {
	groupID, ok := parsePositiveIDParam(c, "id")
	if !ok {
		return
	}
	routeID, ok := parsePositiveIDParam(c, "route_id")
	if !ok {
		return
	}
	if err := h.adminService.DeleteCompositeRoute(c.Request.Context(), groupID, routeID); err != nil {
		response.ErrorFrom(c, err)
		return
	}
	response.Success(c, gin.H{"message": "Composite route deleted"})
}

// PreviewCompositeRoute resolves a model without mutating routes.
// POST /api/v1/admin/groups/:id/composite-routes/preview
func (h *GroupHandler) PreviewCompositeRoute(c *gin.Context) {
	groupID, ok := parsePositiveIDParam(c, "id")
	if !ok {
		return
	}
	var req CompositeRoutePreviewRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request body: "+err.Error())
		return
	}
	decision, err := h.adminService.PreviewCompositeRoute(c.Request.Context(), groupID, service.CompositeRoutePreviewRequest{
		Model:    req.Model,
		Endpoint: req.Endpoint,
	})
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}
	response.Success(c, decision)
}

func compositeRouteRequestToInput(req CompositeRouteRequest, defaultEnabled bool) service.CompositeRouteInput {
	enabled := defaultEnabled
	if req.Enabled != nil {
		enabled = *req.Enabled
	}
	return service.CompositeRouteInput{
		PublicModel:    req.PublicModel,
		MatchType:      req.MatchType,
		TargetPlatform: req.TargetPlatform,
		UpstreamModel:  req.UpstreamModel,
		Endpoint:       req.Endpoint,
		Priority:       req.Priority,
		Enabled:        enabled,
		Notes:          req.Notes,
	}
}

func parsePositiveIDParam(c *gin.Context, name string) (int64, bool) {
	raw := c.Param(name)
	id, err := strconv.ParseInt(raw, 10, 64)
	if err != nil || id <= 0 {
		response.BadRequest(c, "Invalid "+name)
		return 0, false
	}
	return id, true
}

// GetAll handles getting all active groups without pagination.
// Pass ?include_inactive=true to also include disabled groups (used by the
// API Key group filter, which needs to surface groups that still have API keys
// bound to them even after the group is disabled).
// GET /api/v1/admin/groups/all
func (h *GroupHandler) GetAll(c *gin.Context) {
	platform := c.Query("platform")
	includeInactive := c.Query("include_inactive") == "true"

	var groups []service.Group
	var err error

	if includeInactive {
		groups, err = h.adminService.GetAllGroupsIncludingInactive(c.Request.Context())
	} else if platform != "" {
		groups, err = h.adminService.GetAllGroupsByPlatform(c.Request.Context(), platform)
	} else {
		groups, err = h.adminService.GetAllGroups(c.Request.Context())
	}

	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	outGroups := make([]dto.AdminGroup, 0, len(groups))
	for i := range groups {
		outGroups = append(outGroups, *dto.GroupFromServiceAdmin(&groups[i]))
	}
	response.Success(c, outGroups)
}

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

	group, err := h.adminService.GetGroup(c.Request.Context(), groupID)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, dto.GroupFromServiceAdmin(group))
}

// GetModelsListCandidates handles getting candidate model IDs for custom /v1/models list.
// GET /api/v1/admin/groups/:id/models-list-candidates
func (h *GroupHandler) GetModelsListCandidates(c *gin.Context) {
	groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil || groupID < 0 {
		response.BadRequest(c, "Invalid group ID")
		return
	}

	models, err := h.adminService.GetGroupModelsListCandidates(
		c.Request.Context(),
		groupID,
		c.Query("platform"),
	)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, gin.H{"models": models})
}

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

	if err := service.ValidatePeakRateConfig(req.SubscriptionType, req.PeakRateEnabled, req.PeakStart, req.PeakEnd, float64ValueOrDefault(req.PeakRateMultiplier, 1.0)); err != nil {
		response.BadRequest(c, err.Error())
		return
	}

	// platform 是 omitempty：预校验必须用与 CreateGroup 落库一致的归一化平台，
	// 否则省略 platform 的请求会被误判成「平台不支持利润控制」。
	if err := service.ValidateProfitControlConfig(service.NormalizeGroupPlatform(req.Platform), req.ProfitControlEnabled, float64ValueOrDefault(req.ProfitMinMargin, 0), float64ValueOrDefault(req.ProfitSafetyBuffer, 0)); err != nil {
		response.BadRequest(c, err.Error())
		return
	}

	group, err := h.adminService.CreateGroup(c.Request.Context(), &service.CreateGroupInput{
		Name:                            req.Name,
		Description:                     req.Description,
		Platform:                        req.Platform,
		RateMultiplier:                  req.RateMultiplier,
		IsExclusive:                     req.IsExclusive,
		SubscriptionType:                req.SubscriptionType,
		DailyLimitUSD:                   req.DailyLimitUSD.ToServiceInput(),
		WeeklyLimitUSD:                  req.WeeklyLimitUSD.ToServiceInput(),
		MonthlyLimitUSD:                 req.MonthlyLimitUSD.ToServiceInput(),
		LongContextPricingEnabled:       req.LongContextPricingEnabled,
		ModelPricing:                    req.ModelPricing,
		AllowImageGeneration:            req.AllowImageGeneration,
		AllowBatchImageGeneration:       req.AllowBatchImageGeneration,
		ImageRateIndependent:            req.ImageRateIndependent,
		ImageRateMultiplier:             req.ImageRateMultiplier,
		BatchImageDiscountMultiplier:    req.BatchImageDiscountMultiplier,
		BatchImageHoldMultiplier:        req.BatchImageHoldMultiplier,
		VideoRateIndependent:            req.VideoRateIndependent,
		VideoRateMultiplier:             req.VideoRateMultiplier,
		PeakRateEnabled:                 req.PeakRateEnabled,
		PeakStart:                       req.PeakStart,
		PeakEnd:                         req.PeakEnd,
		PeakRateMultiplier:              req.PeakRateMultiplier,
		ProfitControlEnabled:            req.ProfitControlEnabled,
		ProfitMinMargin:                 req.ProfitMinMargin,
		ProfitSafetyBuffer:              req.ProfitSafetyBuffer,
		ImagePrice1K:                    req.ImagePrice1K,
		ImagePrice2K:                    req.ImagePrice2K,
		ImagePrice4K:                    req.ImagePrice4K,
		VideoPrice480P:                  req.VideoPrice480P,
		VideoPrice720P:                  req.VideoPrice720P,
		VideoPrice1080P:                 req.VideoPrice1080P,
		VideoModelPrices:                req.VideoModelPrices,
		WebSearchPricePerCall:           req.WebSearchPricePerCall,
		SearchPricePer1k:                req.SearchPricePer1k,
		AudioRealtimePricePerMin:        req.AudioRealtimePricePerMin,
		AudioTTSPricePerMillionChars:    req.AudioTtsPricePerMillionChars,
		AudioSTTPricePerHour:            req.AudioSttPricePerHour,
		ClaudeCodeOnly:                  req.ClaudeCodeOnly,
		FallbackGroupID:                 req.FallbackGroupID,
		FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
		ModelRouting:                    req.ModelRouting,
		ModelRoutingEnabled:             req.ModelRoutingEnabled,
		MCPXMLInject:                    req.MCPXMLInject,
		SupportedModelScopes:            req.SupportedModelScopes,
		AllowMessagesDispatch:           req.AllowMessagesDispatch,
		AllowLive:                       req.AllowLive,
		RequireOAuthOnly:                req.RequireOAuthOnly,
		RequirePrivacySet:               req.RequirePrivacySet,
		DefaultMappedModel:              req.DefaultMappedModel,
		MessagesDispatchModelConfig:     req.MessagesDispatchModelConfig,
		ModelsListConfig:                req.ModelsListConfig,
		RPMLimit:                        req.RPMLimit,
		MaxReasoningEffort:              req.MaxReasoningEffort,
		ReasoningEffortMappings:         req.ReasoningEffortMappings,
		CopyAccountsFromGroupIDs:        req.CopyAccountsFromGroupIDs,
	})
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, dto.GroupFromServiceAdmin(group))
}

// Duplicate handles creating an inactive group copy with the source account bindings.
// POST /api/v1/admin/groups/:id/duplicate
func (h *GroupHandler) Duplicate(c *gin.Context) {
	groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil || groupID <= 0 {
		response.BadRequest(c, "Invalid group ID")
		return
	}
	actorScope := adminActorScope(c)

	result, err := executeAdminIdempotent(
		c,
		"admin.groups.duplicate",
		struct {
			GroupID int64 `json:"group_id"`
		}{GroupID: groupID},
		service.DefaultWriteIdempotencyTTL(),
		func(ctx context.Context) (any, error) {
			group, execErr := h.adminService.DuplicateGroup(ctx, groupID, actorScope, c.GetHeader("Idempotency-Key"))
			if execErr != nil {
				return nil, execErr
			}
			return dto.GroupFromServiceAdmin(group), nil
		},
	)
	if err != nil {
		reason := infraerrors.Reason(err)
		if reason == infraerrors.Reason(service.ErrIdempotencyInProgress) || reason == infraerrors.Reason(service.ErrIdempotencyStoreUnavail) {
			recovered, recoverErr := h.adminService.RecoverDuplicateGroup(c.Request.Context(), groupID, actorScope, c.GetHeader("Idempotency-Key"))
			if recoverErr != nil {
				slog.Warn("group_duplicate_recovery_failed", "group_id", groupID, "actor_scope", actorScope, "reason", reason, "error", recoverErr)
			} else if recovered != nil {
				c.Header("X-Idempotency-Recovered", "true")
				response.Success(c, dto.GroupFromServiceAdmin(recovered))
				return
			}
		}
		response.ErrorFrom(c, err)
		return
	}

	if result != nil && result.Replayed {
		c.Header("X-Idempotency-Replayed", "true")
	}
	response.Success(c, result.Data)
}

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

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

	group, err := h.adminService.UpdateGroup(c.Request.Context(), groupID, &service.UpdateGroupInput{
		Name:                            req.Name,
		Description:                     req.Description,
		Platform:                        req.Platform,
		RateMultiplier:                  req.RateMultiplier,
		IsExclusive:                     req.IsExclusive,
		Status:                          req.Status,
		SubscriptionType:                req.SubscriptionType,
		DailyLimitUSD:                   req.DailyLimitUSD.ToServiceInput(),
		WeeklyLimitUSD:                  req.WeeklyLimitUSD.ToServiceInput(),
		MonthlyLimitUSD:                 req.MonthlyLimitUSD.ToServiceInput(),
		LongContextPricingEnabled:       req.LongContextPricingEnabled,
		ModelPricing:                    req.ModelPricing,
		AllowImageGeneration:            req.AllowImageGeneration,
		AllowBatchImageGeneration:       req.AllowBatchImageGeneration,
		ImageRateIndependent:            req.ImageRateIndependent,
		ImageRateMultiplier:             req.ImageRateMultiplier,
		BatchImageDiscountMultiplier:    req.BatchImageDiscountMultiplier,
		BatchImageHoldMultiplier:        req.BatchImageHoldMultiplier,
		VideoRateIndependent:            req.VideoRateIndependent,
		VideoRateMultiplier:             req.VideoRateMultiplier,
		PeakRateEnabled:                 req.PeakRateEnabled,
		PeakStart:                       req.PeakStart,
		PeakEnd:                         req.PeakEnd,
		PeakRateMultiplier:              req.PeakRateMultiplier,
		ProfitControlEnabled:            req.ProfitControlEnabled,
		ProfitMinMargin:                 req.ProfitMinMargin,
		ProfitSafetyBuffer:              req.ProfitSafetyBuffer,
		ImagePrice1K:                    req.ImagePrice1K,
		ImagePrice2K:                    req.ImagePrice2K,
		ImagePrice4K:                    req.ImagePrice4K,
		VideoPrice480P:                  req.VideoPrice480P,
		VideoPrice720P:                  req.VideoPrice720P,
		VideoPrice1080P:                 req.VideoPrice1080P,
		VideoModelPrices:                req.VideoModelPrices,
		WebSearchPricePerCall:           req.WebSearchPricePerCall,
		SearchPricePer1k:                req.SearchPricePer1k,
		AudioRealtimePricePerMin:        req.AudioRealtimePricePerMin,
		AudioTTSPricePerMillionChars:    req.AudioTtsPricePerMillionChars,
		AudioSTTPricePerHour:            req.AudioSttPricePerHour,
		ClaudeCodeOnly:                  req.ClaudeCodeOnly,
		FallbackGroupID:                 req.FallbackGroupID,
		FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
		ModelRouting:                    req.ModelRouting,
		ModelRoutingEnabled:             req.ModelRoutingEnabled,
		MCPXMLInject:                    req.MCPXMLInject,
		SupportedModelScopes:            req.SupportedModelScopes,
		AllowMessagesDispatch:           req.AllowMessagesDispatch,
		AllowLive:                       req.AllowLive,
		RequireOAuthOnly:                req.RequireOAuthOnly,
		RequirePrivacySet:               req.RequirePrivacySet,
		DefaultMappedModel:              req.DefaultMappedModel,
		MessagesDispatchModelConfig:     req.MessagesDispatchModelConfig,
		ModelsListConfig:                req.ModelsListConfig,
		RPMLimit:                        req.RPMLimit,
		MaxReasoningEffort:              req.MaxReasoningEffort,
		ReasoningEffortMappings:         req.ReasoningEffortMappings,
		CopyAccountsFromGroupIDs:        req.CopyAccountsFromGroupIDs,
	})
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, dto.GroupFromServiceAdmin(group))
}

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

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

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

// GetStats handles getting group statistics
// GET /api/v1/admin/groups/:id/stats
func (h *GroupHandler) GetStats(c *gin.Context) {
	groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid group ID")
		return
	}

	// Return mock data for now
	response.Success(c, gin.H{
		"total_api_keys":  0,
		"active_api_keys": 0,
		"total_requests":  0,
		"total_cost":      0.0,
	})
	_ = groupID // TODO: implement actual stats
}

// GetUsageSummary returns today's, yesterday's, and cumulative cost for all groups.
// GET /api/v1/admin/groups/usage-summary
func (h *GroupHandler) GetUsageSummary(c *gin.Context) {
	todayStart := service.GroupUsageTodayStart(time.Now())

	results, err := h.dashboardService.GetGroupUsageSummary(c.Request.Context(), todayStart)
	if err != nil {
		response.Error(c, 500, "Failed to get group usage summary")
		return
	}

	response.Success(c, results)
}

// GetCapacitySummary returns aggregated capacity (concurrency/sessions/RPM) for all active groups.
// GET /api/v1/admin/groups/capacity-summary
func (h *GroupHandler) GetCapacitySummary(c *gin.Context) {
	results, err := h.groupCapacityService.GetAllGroupCapacity(c.Request.Context())
	if err != nil {
		response.Error(c, 500, "Failed to get group capacity summary")
		return
	}
	response.Success(c, results)
}

// GetGroupAPIKeys handles getting API keys in a group
// GET /api/v1/admin/groups/:id/api-keys
func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) {
	groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid group ID")
		return
	}

	page, pageSize := response.ParsePagination(c)

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

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

// GetGroupRateMultipliers handles getting rate multipliers for users in a group
// GET /api/v1/admin/groups/:id/rate-multipliers
func (h *GroupHandler) GetGroupRateMultipliers(c *gin.Context) {
	groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid group ID")
		return
	}

	entries, err := h.adminService.GetGroupRateMultipliers(c.Request.Context(), groupID)
	if err != nil {
		response.ErrorFrom(c, err)
		return
	}

	if entries == nil {
		entries = []service.UserGroupRateEntry{}
	}
	response.Success(c, entries)
}

// ClearGroupRateMultipliers handles clearing all rate multipliers for a group
// DELETE /api/v1/admin/groups/:id/rate-multipliers
func (h *GroupHandler) ClearGroupRateMultipliers(c *gin.Context) {
	groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid group ID")
		return
	}

	if err := h.adminService.ClearGroupRateMultipliers(c.Request.Context(), groupID); err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, gin.H{"message": "Rate multipliers cleared successfully"})
}

// BatchSetGroupRateMultipliersRequest represents batch set rate multipliers request
type BatchSetGroupRateMultipliersRequest struct {
	Entries []service.GroupRateMultiplierInput `json:"entries" binding:"required"`
}

// BatchSetGroupRateMultipliers handles batch setting rate multipliers for a group
// PUT /api/v1/admin/groups/:id/rate-multipliers
func (h *GroupHandler) BatchSetGroupRateMultipliers(c *gin.Context) {
	groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid group ID")
		return
	}

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

	if err := h.adminService.BatchSetGroupRateMultipliers(c.Request.Context(), groupID, req.Entries); err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, gin.H{"message": "Rate multipliers updated successfully"})
}

// BatchSetGroupRPMOverridesRequest represents batch set rpm_override request
type BatchSetGroupRPMOverridesRequest struct {
	Entries []service.GroupRPMOverrideInput `json:"entries" binding:"required"`
}

// BatchSetGroupRPMOverrides handles batch setting rpm_override for users in a group
// PUT /api/v1/admin/groups/:id/rpm-overrides
func (h *GroupHandler) BatchSetGroupRPMOverrides(c *gin.Context) {
	groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid group ID")
		return
	}

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

	if err := h.adminService.BatchSetGroupRPMOverrides(c.Request.Context(), groupID, req.Entries); err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, gin.H{"message": "RPM overrides updated successfully"})
}

// ClearGroupRPMOverrides handles clearing all rpm_override for a group
// DELETE /api/v1/admin/groups/:id/rpm-overrides
func (h *GroupHandler) ClearGroupRPMOverrides(c *gin.Context) {
	groupID, err := strconv.ParseInt(c.Param("id"), 10, 64)
	if err != nil {
		response.BadRequest(c, "Invalid group ID")
		return
	}

	if err := h.adminService.ClearGroupRPMOverrides(c.Request.Context(), groupID); err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, gin.H{"message": "RPM overrides cleared successfully"})
}

// UpdateSortOrderRequest represents the request to update group sort orders
type UpdateSortOrderRequest struct {
	Updates []struct {
		ID        int64 `json:"id" binding:"required"`
		SortOrder int   `json:"sort_order"`
	} `json:"updates" binding:"required,min=1"`
}

// UpdateSortOrder handles updating group sort orders
// PUT /api/v1/admin/groups/sort-order
func (h *GroupHandler) UpdateSortOrder(c *gin.Context) {
	var req UpdateSortOrderRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "Invalid request: "+err.Error())
		return
	}

	updates := make([]service.GroupSortOrderUpdate, 0, len(req.Updates))
	for _, u := range req.Updates {
		updates = append(updates, service.GroupSortOrderUpdate{
			ID:        u.ID,
			SortOrder: u.SortOrder,
		})
	}

	if err := h.adminService.UpdateGroupSortOrders(c.Request.Context(), updates); err != nil {
		response.ErrorFrom(c, err)
		return
	}

	response.Success(c, gin.H{"message": "Sort order updated successfully"})
}
