package runner

import (
	"context"
	"math"
	"strconv"
	"strings"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

const videoBillingUnitSeconds = 5

type EstimateResult struct {
	Items              []any   `json:"items"`
	Resolver           string  `json:"resolver"`
	TotalAmount        float64 `json:"totalAmount"`
	ReservationAmount  float64 `json:"reservationAmount"`
	Currency           string  `json:"currency"`
	CandidateCount     int     `json:"candidateCount"`
	PricingVersion     string  `json:"pricingVersion"`
	RequestFingerprint string  `json:"requestFingerprint"`
}

func (s *Service) Estimate(ctx context.Context, kind string, model string, body map[string]any, user *auth.User) (EstimateResult, error) {
	candidates, body, err := s.candidatesForRequest(ctx, kind, model, body, user)
	if err != nil {
		return EstimateResult{}, err
	}
	estimates := make([]candidateEstimate, 0, len(candidates))
	for _, candidate := range candidates {
		preprocessing := s.preprocessRequestWithScripts(ctx, kind, body, candidate)
		if preprocessing.Err != nil {
			return EstimateResult{}, parameterPreprocessClientError(preprocessing.Err)
		}
		estimate, candidateErr := s.estimateCandidateV2(ctx, user, kind, preprocessing.Body, candidate)
		if candidateErr != nil {
			return EstimateResult{}, candidateErr
		}
		estimates = append(estimates, estimate)
	}
	return buildEstimateResult(estimates, pricingRequestFingerprint(kind, model, body))
}

// ValidateModelAccess resolves the same permission-filtered candidates used by
// execution before a task row is created. This keeps authorization failures
// out of task history while execution still re-resolves candidates to avoid
// using stale routing or capacity state.
func (s *Service) ValidateModelAccess(ctx context.Context, kind string, model string, body map[string]any, user *auth.User) error {
	_, _, err := s.candidatesForRequest(ctx, kind, model, body, user)
	if store.ModelCandidateRetryAfter(err) > 0 {
		s.observeCandidateRouting("cooldown_skipped")
	}
	return err
}

func (s *Service) candidatesForRequest(ctx context.Context, kind string, model string, body map[string]any, user *auth.User) ([]store.RuntimeModelCandidate, map[string]any, error) {
	body = normalizeRequest(kind, body)
	modelType := modelTypeFromKind(kind, body)
	candidates, err := s.store.ListModelCandidates(ctx, model, modelType, user)
	if err != nil {
		return nil, body, err
	}
	candidates, err = filterCandidatesByRequestedPlatform(candidates, body)
	if err != nil {
		return nil, body, err
	}
	candidates, _, err = filterRuntimeCandidatesByRequest(kind, model, modelType, body, candidates)
	if err != nil {
		return nil, body, err
	}
	candidates, _, err = filterRuntimeCandidatesByOutputTokens(kind, model, modelType, body, candidates)
	if err != nil {
		return nil, body, err
	}
	return candidates, body, nil
}

func (s *Service) estimatedBillings(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate) []any {
	usage := clients.Usage{InputTokens: estimateRequestTokens(body)}
	if isTextGenerationKind(kind) {
		usage.OutputTokens = int(floatFromAny(body["max_tokens"]))
	}
	if isTextGenerationKind(kind) && usage.OutputTokens == 0 {
		usage.OutputTokens = 64
	}
	usage.TotalTokens = usage.InputTokens + usage.OutputTokens
	response := clients.Response{Usage: usage, Result: map[string]any{"usage": map[string]any{
		"prompt_tokens":     usage.InputTokens,
		"completion_tokens": usage.OutputTokens,
		"total_tokens":      usage.TotalTokens,
	}}}
	return s.billings(ctx, user, kind, body, candidate, response, true)
}

func (s *Service) billings(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool) []any {
	config := s.effectiveBillingConfig(ctx, candidate)
	discount := effectiveDiscount(ctx, s.store, user, candidate)
	if isTextBillingKind(kind) {
		inputTokens := response.Usage.InputTokens
		outputTokens := response.Usage.OutputTokens
		cachedInputTokens := response.Usage.CachedInputTokens
		if isTextInputOnlyKind(kind) && inputTokens == 0 && response.Usage.TotalTokens > 0 {
			inputTokens = response.Usage.TotalTokens
		}
		if inputTokens == 0 && outputTokens == 0 {
			inputTokens = estimateRequestTokens(body)
			if isTextGenerationKind(kind) {
				outputTokens = 1
			}
		}
		if cachedInputTokens > inputTokens && inputTokens > 0 {
			cachedInputTokens = inputTokens
		}
		uncachedInputTokens := inputTokens - cachedInputTokens
		if uncachedInputTokens < 0 {
			uncachedInputTokens = 0
		}
		inputPrice := resourcePrice(config, "text", "textInputPer1k", "inputTokenPrice", "basePrice")
		cachedInputPrice := resourcePrice(config, "text", "textCachedInputPer1k", "cachedInputTokenPrice", "cacheInputTokenPrice", "textInputCacheHitPer1k", "inputCacheHitTokenPrice")
		if cachedInputPrice <= 0 {
			cachedInputPrice = inputPrice / 10
		}
		inputAmount := roundPrice(float64(uncachedInputTokens) / 1000 * inputPrice * discount)
		lines := []any{}
		if uncachedInputTokens > 0 || cachedInputTokens == 0 {
			lines = append(lines, billingLineWithDetails(candidate, "text_input", "1k_tokens", uncachedInputTokens, inputAmount, discount, simulated, map[string]any{
				"inputTokens":         inputTokens,
				"uncachedInputTokens": uncachedInputTokens,
				"cachedInputTokens":   cachedInputTokens,
				"pricePer1k":          inputPrice,
			}))
		}
		if cachedInputTokens > 0 {
			cachedInputAmount := roundPrice(float64(cachedInputTokens) / 1000 * cachedInputPrice * discount)
			lines = append(lines, billingLineWithDetails(candidate, "text_cached_input", "1k_tokens", cachedInputTokens, cachedInputAmount, discount, simulated, map[string]any{
				"inputTokens":         inputTokens,
				"uncachedInputTokens": uncachedInputTokens,
				"cachedInputTokens":   cachedInputTokens,
				"pricePer1k":          cachedInputPrice,
			}))
		}
		if isTextGenerationKind(kind) {
			outputAmount := roundPrice(float64(outputTokens) / 1000 * resourcePrice(config, "text", "textOutputPer1k", "outputTokenPrice", "basePrice") * discount)
			lines = append(lines, billingLine(candidate, "text_output", "1k_tokens", outputTokens, outputAmount, discount, simulated))
		}
		return lines
	}
	count := requestOutputCount(body)
	resource := "image"
	unit := "image"
	baseKey := "imageBase"
	if kind == "images.edits" {
		resource = "image_edit"
		baseKey = "editBase"
	}
	if kind == "images.vectorize" {
		resource = "image_vectorize"
		unit = "conversion"
		baseKey = "vectorizeBase"
	}
	if kind == "videos.upscales" {
		resource = "video_enhance"
		unit = "5s_video"
		baseKey = "videoEnhanceBase"
		inputs := resolveVideoEnhancePricingInputs(body, response)
		amount, details := videoEnhanceAmount(config, inputs, float64(count), resourcePrice(config, resource, baseKey, "basePrice"))
		amount = math.Ceil(amount*1e8) / 1e8 * discount
		return []any{billingLineWithDetails(candidate, resource, unit, float64(count)*inputs.DurationUnits, roundPrice(amount), discount, simulated, details)}
	}
	if kind == "videos.generations" {
		resource = "video"
		unit = "5s_video"
		baseKey = "videoBase"
		duration, durationSource := billingDurationSeconds(body, response)
		audioEnabled, audioSource := billingAudioEnabled(body, response)
		durationUnits := videoDurationUnits(duration)
		amount := float64(count) *
			durationUnits *
			resourcePrice(config, resource, baseKey, "basePrice") *
			resourceWeight(config, resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size"))) *
			resourceWeight(config, resource, "audioWeights", boolWeightKey(audioEnabled)) *
			resourceWeight(config, resource, "referenceVideoWeights", boolWeightKey(requestHasReferenceVideo(body))) *
			resourceWeight(config, resource, "voiceSpecifiedWeights", boolWeightKey(requestHasVoiceID(body, audioEnabled))) *
			discount
		return []any{billingLineWithDetails(candidate, resource, unit, videoDurationQuantity(duration, count), roundPrice(amount), discount, simulated, map[string]any{
			"count":             count,
			"audio":             audioEnabled,
			"audioSource":       audioSource,
			"durationSeconds":   duration,
			"durationSource":    durationSource,
			"durationUnit":      "5s",
			"durationUnitCount": durationUnits,
		})}
	}
	if kind == "song.generations" || kind == "music.generations" {
		resource = "music"
		unit = "song"
		baseKey = "musicBase"
		amount := float64(count) * resourcePrice(config, resource, baseKey, "basePrice") * discount
		return []any{billingLine(candidate, resource, unit, count, roundPrice(amount), discount, simulated)}
	}
	if kind == "speech.generations" {
		resource = "audio"
		unit = "character"
		baseKey = "audioBase"
		quantity := len([]rune(stringFromMap(body, "text")))
		if quantity <= 0 {
			quantity = 1
		}
		amount := float64(quantity) * resourcePrice(config, resource, baseKey, "basePrice") * discount
		return []any{billingLine(candidate, resource, unit, quantity, roundPrice(amount), discount, simulated)}
	}
	if kind == "voice.clone" {
		text := stringFromMap(body, "text")
		if strings.TrimSpace(text) == "" {
			return nil
		}
		resource = "audio"
		unit = "character"
		baseKey = "audioBase"
		quantity := len([]rune(text))
		if quantity <= 0 {
			quantity = 1
		}
		amount := float64(quantity) * resourcePrice(config, resource, baseKey, "basePrice") * discount
		return []any{billingLineWithDetails(candidate, resource, unit, quantity, roundPrice(amount), discount, simulated, map[string]any{"preview": true})}
	}
	amount := float64(count) * resourcePrice(config, resource, baseKey, "basePrice") * resourceWeight(config, resource, "qualityWeights", stringFromMap(body, "quality")) * resourceWeight(config, resource, "sizeWeights", stringFromMap(body, "size")) * resourceWeight(config, resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size"))) * discount
	return []any{billingLine(candidate, resource, unit, count, roundPrice(amount), discount, simulated)}
}

type videoEnhancePricingInputs struct {
	DurationSeconds, DurationUnits, SourceFrameRate, TargetFrameRate, SlowMotionRate float64
	SourceResolution, TargetResolution, Model                                        string
}

var videoEnhanceResolutionPixels = map[string]float64{
	"480p": 854 * 480, "720p": 1280 * 720, "1080p": 1920 * 1080,
	"1440p": 2560 * 1440, "2160p": 3840 * 2160,
}

var videoEnhanceResolutionBaseline = map[string]float64{"480p": 1, "720p": 1.4, "1080p": 1.8, "1440p": 2.1, "2160p": 2.6}

func resolveVideoEnhancePricingInputs(body map[string]any, response clients.Response) videoEnhancePricingInputs {
	merged := cloneMap(body)
	if data, ok := response.Result["data"].([]any); ok && len(data) > 0 {
		if item, ok := data[0].(map[string]any); ok {
			for _, key := range []string{"duration", "source_resolution", "target_resolution", "source_frame_rate", "target_frame_rate", "slow_motion_rate"} {
				if merged[key] == nil && item[key] != nil {
					merged[key] = item[key]
				}
			}
		}
	}
	duration := floatFromAny(merged["duration"])
	if duration <= 0 {
		duration = 5
	}
	targetValue := firstNonEmptyString(stringFromMap(merged, "target_resolution"), stringFromMap(merged, "output_resolution"), stringFromMap(merged, "resolution"))
	if dimensions := videoEnhanceDimensionsValue(merged, "output_width", "output_height"); dimensions != "" {
		targetValue = dimensions
	}
	target := normalizeVideoEnhanceResolution(targetValue, "1080p")
	sourceValue := firstNonEmptyString(stringFromMap(merged, "source_resolution"), stringFromMap(merged, "resolution"))
	if dimensions := videoEnhanceDimensionsValue(merged, "source_width", "source_height"); dimensions != "" {
		sourceValue = dimensions
	}
	source := normalizeVideoEnhanceResolution(sourceValue, target)
	sourceFPS := floatFromAny(firstPresentValue(merged, "source_frame_rate", "frame_rate", "frameRate"))
	if sourceFPS <= 0 {
		sourceFPS = 24
	}
	targetFPS := floatFromAny(firstPresentValue(merged, "target_frame_rate", "output_frame_rate"))
	if targetFPS <= 0 {
		targetFPS = sourceFPS
	}
	slowMotion := floatFromAny(merged["slow_motion_rate"])
	if slowMotion <= 0 {
		slowMotion = 1
	}
	return videoEnhancePricingInputs{
		DurationSeconds: duration, DurationUnits: math.Max(1, math.Round(duration)/5),
		SourceResolution: source, TargetResolution: target,
		SourceFrameRate: sourceFPS, TargetFrameRate: targetFPS, SlowMotionRate: slowMotion,
		Model: firstNonEmptyString(stringFromMap(merged, "model"), stringFromMap(merged, "enhancement_model")),
	}
}

func videoEnhanceDimensionsValue(values map[string]any, widthKey, heightKey string) string {
	width := int(math.Round(floatFromAny(values[widthKey])))
	height := int(math.Round(floatFromAny(values[heightKey])))
	if width <= 0 || height <= 0 {
		return ""
	}
	return strconv.Itoa(width) + "x" + strconv.Itoa(height)
}

func normalizeVideoEnhanceResolution(value, fallback string) string {
	normalized := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(value)), "_upscale")
	if normalized == "2k" {
		normalized = "1440p"
	}
	if normalized == "4k" {
		normalized = "2160p"
	}
	if width, height, ok := parseVideoEnhanceDimensions(normalized); ok {
		pixels := float64(width * height)
		for _, bucket := range []string{"480p", "720p", "1080p", "1440p", "2160p"} {
			if pixels <= videoEnhanceResolutionPixels[bucket] {
				return bucket
			}
		}
		return "2160p"
	}
	if _, ok := videoEnhanceResolutionPixels[normalized]; ok {
		return normalized
	}
	if _, ok := videoEnhanceResolutionPixels[fallback]; ok {
		return fallback
	}
	return "1080p"
}

func parseVideoEnhanceDimensions(value string) (int, int, bool) {
	parts := strings.Split(strings.ReplaceAll(value, " ", ""), "x")
	if len(parts) != 2 {
		return 0, 0, false
	}
	width, widthErr := strconv.Atoi(parts[0])
	height, heightErr := strconv.Atoi(parts[1])
	return width, height, widthErr == nil && heightErr == nil && width > 0 && height > 0
}

func videoEnhanceAmount(config map[string]any, input videoEnhancePricingInputs, count, basePrice float64) (float64, map[string]any) {
	transitionKey := input.SourceResolution + "->" + input.TargetResolution
	minimumResolutionWeight := videoEnhanceResolutionBaseline[input.TargetResolution]
	resolutionWeight, configuredResolution := pricingDynamicWeight(config, "video_enhance", "resolutionTransitions", transitionKey)
	if !configuredResolution {
		resolutionWeight = math.Max(minimumResolutionWeight, videoEnhanceResolutionPixels[input.TargetResolution]/videoEnhanceResolutionPixels[input.SourceResolution])
	} else {
		resolutionWeight = math.Max(minimumResolutionWeight, resolutionWeight)
	}
	sourceFPS := math.Max(1, math.Round(input.SourceFrameRate))
	targetFPS := math.Max(1, math.Round(input.TargetFrameRate))
	frameKey := strconv.Itoa(int(sourceFPS)) + "->" + strconv.Itoa(int(targetFPS))
	minimumFrameWeight := math.Max(1, targetFPS/24)
	frameWeight, configuredFrame := pricingDynamicWeight(config, "video_enhance", "frameRateTransitions", frameKey)
	if !configuredFrame {
		frameWeight = minimumFrameWeight
	} else {
		frameWeight = math.Max(minimumFrameWeight, frameWeight)
	}
	modelWeight, configuredModel := pricingDynamicWeight(config, "video_enhance", "modelWeights", input.Model)
	if !configuredModel {
		modelWeight = 1
	}
	markup, foundMarkup := pricingDynamicWeight(config, "video_enhance", "", "markup")
	if !foundMarkup {
		markup = 1
	}
	amount := count * input.DurationUnits * basePrice * resolutionWeight * frameWeight * input.SlowMotionRate * modelWeight * markup
	return amount, map[string]any{
		"count": count, "durationSeconds": input.DurationSeconds, "durationUnit": "5s", "durationUnitCount": input.DurationUnits,
		"sourceResolution": input.SourceResolution, "targetResolution": input.TargetResolution,
		"resolutionTransitionKey": transitionKey, "resolutionTransitionWeight": resolutionWeight, "resolutionTransitionFallback": !configuredResolution,
		"sourceFrameRate": input.SourceFrameRate, "targetFrameRate": input.TargetFrameRate,
		"frameRateTransitionKey": frameKey, "frameRateTransitionWeight": frameWeight, "frameRateTransitionFallback": !configuredFrame,
		"slowMotionRate": input.SlowMotionRate, "model": input.Model, "modelWeight": modelWeight, "markup": markup,
	}
}

func pricingDynamicWeight(config map[string]any, resource, group, key string) (float64, bool) {
	resourceConfig, _ := config[resource].(map[string]any)
	dynamic, _ := resourceConfig["dynamicWeight"].(map[string]any)
	if group == "" {
		value, ok := dynamic[key]
		weight := floatFromAny(value)
		return weight, ok && weight > 0
	}
	values, _ := dynamic[group].(map[string]any)
	value, ok := values[key]
	weight := floatFromAny(value)
	return weight, ok && weight > 0
}

func (s *Service) effectiveBillingConfig(ctx context.Context, candidate store.RuntimeModelCandidate) map[string]any {
	var inheritedRuleSetConfig map[string]any
	if ruleSetID := firstNonEmptyString(candidate.BasePricingRuleSetID, candidate.PlatformPricingRuleSetID); ruleSetID != "" && s.store != nil {
		if ruleSetConfig, err := s.store.PricingRuleSetBillingConfig(ctx, ruleSetID); err == nil && len(ruleSetConfig) > 0 {
			inheritedRuleSetConfig = ruleSetConfig
		}
	}
	var modelRuleSetConfig map[string]any
	if candidate.ModelPricingRuleSetID != "" && s.store != nil {
		if ruleSetConfig, err := s.store.PricingRuleSetBillingConfig(ctx, candidate.ModelPricingRuleSetID); err == nil && len(ruleSetConfig) > 0 {
			modelRuleSetConfig = ruleSetConfig
		}
	}
	return store.ResolveEffectiveBillingConfig(store.EffectiveBillingConfigInput{
		BaseConfig:                candidate.BaseBillingConfig,
		LegacyPlatformModelConfig: candidate.BillingConfig,
		InheritedRuleSetConfig:    inheritedRuleSetConfig,
		ModelRuleSetConfig:        modelRuleSetConfig,
		Override:                  candidate.BillingConfigOverride,
	})
}

func effectiveDiscount(ctx context.Context, db *store.Store, user *auth.User, candidate store.RuntimeModelCandidate) float64 {
	discount := candidate.DefaultDiscountFactor
	if candidate.DiscountFactor > 0 {
		discount = candidate.DiscountFactor
	}
	if discount <= 0 {
		discount = 1
	}
	if db != nil {
		if group, err := db.ResolveUserGroupPolicy(ctx, user); err == nil {
			groupDiscount := floatFromAny(group.BillingDiscountPolicy["discountFactor"])
			if groupDiscount > 0 {
				discount *= groupDiscount
			}
		}
	}
	return discount
}

func billingLine(candidate store.RuntimeModelCandidate, resourceType string, unit string, quantity any, amount float64, discount float64, simulated bool) map[string]any {
	return billingLineWithDetails(candidate, resourceType, unit, quantity, amount, discount, simulated, nil)
}

func billingLineWithDetails(candidate store.RuntimeModelCandidate, resourceType string, unit string, quantity any, amount float64, discount float64, simulated bool, details map[string]any) map[string]any {
	line := map[string]any{
		"model":           candidate.ModelName,
		"modelAlias":      candidate.ModelAlias,
		"provider":        candidate.Provider,
		"platformId":      candidate.PlatformID,
		"platformModelId": candidate.PlatformModelID,
		"resourceType":    resourceType,
		"unit":            unit,
		"quantity":        quantity,
		"amount":          amount,
		"currency":        "resource",
		"discountFactor":  discount,
		"simulated":       simulated,
	}
	for key, value := range details {
		line[key] = value
	}
	return line
}

func price(config map[string]any, key string) float64 {
	value := floatFromAny(config[key])
	if value > 0 {
		return value
	}
	return 0
}

func resourcePrice(config map[string]any, resource string, keys ...string) float64 {
	for _, key := range keys {
		if value := price(config, key); value > 0 {
			return value
		}
	}
	if resourceConfig, ok := config[resource].(map[string]any); ok {
		for _, key := range keys {
			if value := floatFromAny(resourceConfig[key]); value > 0 {
				return value
			}
		}
		if formulaConfig, ok := resourceConfig["formulaConfig"].(map[string]any); ok {
			for _, key := range keys {
				if value := floatFromAny(formulaConfig[key]); value > 0 {
					return value
				}
			}
		}
		if value := floatFromAny(resourceConfig["basePrice"]); value > 0 {
			return value
		}
	}
	if resource == "text" {
		if value := resourcePrice(config, "text_total", keys...); value > 0 {
			return value
		}
	}
	if resource == "image_edit" {
		return resourcePrice(config, "image", keys...)
	}
	return 0
}

func weighted(config map[string]any, key string, name string) float64 {
	if strings.TrimSpace(name) == "" {
		return 1
	}
	weights, _ := config[key].(map[string]any)
	if value := floatFromAny(weights[name]); value > 0 {
		return value
	}
	return 1
}

func resourceWeight(config map[string]any, resource string, key string, name string) float64 {
	keys := weightKeyAliases(key)
	names := weightValueAliases(key, name)
	for _, candidateKey := range keys {
		for _, candidateName := range names {
			if value := weighted(config, candidateKey, candidateName); value != 1 {
				return value
			}
		}
	}
	if value := dynamicWeight(config["dynamicWeight"], keys, names); value != 1 {
		return value
	}
	if strings.TrimSpace(name) == "" {
		return 1
	}
	resourceConfig, _ := config[resource].(map[string]any)
	if len(resourceConfig) == 0 && resource == "image_edit" {
		resourceConfig, _ = config["image"].(map[string]any)
	}
	if value := dynamicWeight(resourceConfig["dynamicWeight"], keys, names); value != 1 {
		return value
	}
	for _, candidateKey := range keys {
		if weights, ok := resourceConfig[candidateKey].(map[string]any); ok {
			for _, candidateName := range names {
				if value := floatFromAny(weights[candidateName]); value > 0 {
					return value
				}
			}
		}
	}
	return 1
}

func dynamicWeight(value any, keys []string, names []string) float64 {
	if len(names) == 0 {
		return 1
	}
	weights, _ := value.(map[string]any)
	if len(weights) == 0 {
		return 1
	}
	for _, name := range names {
		if direct := floatFromAny(weights[name]); direct > 0 {
			return direct
		}
	}
	for _, key := range keys {
		if nested, ok := weights[key].(map[string]any); ok {
			for _, name := range names {
				if nestedValue := floatFromAny(nested[name]); nestedValue > 0 {
					return nestedValue
				}
			}
		}
	}
	return 1
}

func weightKeyAliases(key string) []string {
	switch key {
	case "qualityWeights":
		return []string{"qualityWeights", "qualityFactors"}
	case "resolutionWeights":
		return []string{"resolutionWeights", "resolutionFactors"}
	case "audioWeights":
		return []string{"audioWeights", "audioFactors"}
	case "referenceVideoWeights":
		return []string{"referenceVideoWeights", "referenceVideoFactors"}
	case "voiceSpecifiedWeights":
		return []string{"voiceSpecifiedWeights", "voiceSpecifiedFactors"}
	default:
		return []string{key}
	}
}

func weightValueAliases(key string, name string) []string {
	name = strings.TrimSpace(name)
	if name == "" {
		return nil
	}
	switch key {
	case "resolutionWeights":
		aliases := []string{name}
		lower := strings.ToLower(name)
		if lower != name {
			aliases = append(aliases, lower)
		}
		if strings.HasSuffix(lower, "p") {
			aliases = append(aliases, strings.TrimSuffix(lower, "p")+"P")
		}
		if lower == "768p" {
			aliases = append(aliases, "720p")
		}
		return aliases
	case "audioWeights":
		return []string{name, "audio-" + name}
	case "referenceVideoWeights":
		return []string{name, "reference-video-" + name}
	case "voiceSpecifiedWeights":
		return []string{name, "voice-specified-" + name}
	default:
		return []string{name}
	}
}

func videoDurationUnits(durationSeconds float64) float64 {
	return videoDurationQuantity(durationSeconds, 1)
}

func videoDurationQuantity(durationSeconds float64, count int) float64 {
	const durationPrecision = 1_000_000_000
	scaledDuration := math.Round(durationSeconds * durationPrecision)
	return scaledDuration * float64(count) / (durationPrecision * videoBillingUnitSeconds)
}

func requestOutputCount(body map[string]any) int {
	for _, key := range []string{"n", "count", "batch_size", "batchSize"} {
		if value := int(math.Ceil(floatFromAny(body[key]))); value > 0 {
			return value
		}
	}
	return 1
}

func requestDurationSeconds(body map[string]any) float64 {
	for _, key := range []string{"duration", "durationSeconds", "duration_seconds"} {
		if value := floatFromAny(body[key]); value > 0 {
			return value
		}
	}
	for _, value := range body {
		items, ok := value.([]any)
		if !ok || len(items) == 0 {
			continue
		}
		total := 0.0
		allDurationItems := true
		for _, item := range items {
			record, ok := item.(map[string]any)
			if !ok {
				allDurationItems = false
				break
			}
			duration := floatFromAny(record["duration"])
			if duration <= 0 {
				allDurationItems = false
				break
			}
			total += duration
		}
		if allDurationItems && total > 0 {
			return total
		}
	}
	return 5
}

func billingDurationSeconds(body map[string]any, response clients.Response) (float64, string) {
	if duration, ok := generatedVideoDurationSeconds(response.Result); ok {
		return duration, "generated_video"
	}
	return requestDurationSeconds(body), "preprocessed_request"
}

func generatedVideoDurationSeconds(result map[string]any) (float64, bool) {
	data, _ := result["data"].([]any)
	for _, raw := range data {
		item, _ := raw.(map[string]any)
		if len(item) == 0 {
			continue
		}
		duration := floatFromAny(item["duration"])
		if duration <= 0 {
			continue
		}
		return duration, true
	}
	return 0, false
}

func billingAudioEnabled(body map[string]any, response clients.Response) (bool, string) {
	if value, ok := generatedVideoHasAudio(response.Result); ok {
		return value, "generated_video"
	}
	return boolishValue(body["audio"]), "preprocessed_request"
}

func generatedVideoHasAudio(result map[string]any) (bool, bool) {
	data, _ := result["data"].([]any)
	for _, raw := range data {
		item, _ := raw.(map[string]any)
		if len(item) == 0 {
			continue
		}
		if value, ok := boolishOptional(firstPresentValue(item, "has_audio", "hasAudio")); ok {
			return value, true
		}
	}
	return false, false
}

func requestHasReferenceVideo(body map[string]any) bool {
	if hasNonEmptyArray(body["video_list"]) || hasNonEmptyArray(body["videoList"]) {
		return true
	}
	if firstNonEmptyStringValue(body, "video", "video_url", "videoUrl", "reference_video", "referenceVideo") != "" {
		return true
	}
	content, _ := body["content"].([]any)
	for _, item := range content {
		record, _ := item.(map[string]any)
		if len(record) == 0 {
			continue
		}
		itemType := strings.TrimSpace(stringFromAny(record["type"]))
		role := strings.TrimSpace(stringFromAny(record["role"]))
		if itemType == "video_url" || role == "video_feature" || role == "video_base" || role == "reference_video" {
			return true
		}
	}
	return false
}

func requestHasVoiceID(body map[string]any, audioEnabled bool) bool {
	return audioEnabled && firstNonEmptyStringValue(body, "voice_id", "voiceId") != ""
}

func boolWeightKey(value bool) string {
	if value {
		return "true"
	}
	return "false"
}

func boolishValue(value any) bool {
	result, _ := boolishOptional(value)
	return result
}

func boolishOptional(value any) (bool, bool) {
	switch typed := value.(type) {
	case bool:
		return typed, true
	case string:
		switch strings.ToLower(strings.TrimSpace(typed)) {
		case "true", "1", "yes", "on":
			return true, true
		case "false", "0", "no", "off":
			return false, true
		}
	case int:
		return typed != 0, true
	case int64:
		return typed != 0, true
	case float64:
		return typed != 0, true
	}
	return false, false
}

func firstPresentValue(record map[string]any, keys ...string) any {
	for _, key := range keys {
		if value, ok := record[key]; ok {
			return value
		}
	}
	return nil
}

func hasNonEmptyArray(value any) bool {
	items, ok := value.([]any)
	return ok && len(items) > 0
}

func totalBillingAmount(items []any) float64 {
	total := 0.0
	for _, raw := range items {
		line, _ := raw.(map[string]any)
		total += floatFromAny(line["amount"])
	}
	return roundPrice(total)
}

func billingCurrency(items []any) string {
	for _, raw := range items {
		line, _ := raw.(map[string]any)
		if currency := stringFromAny(line["currency"]); currency != "" {
			return currency
		}
	}
	return "resource"
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

func roundPrice(value float64) float64 {
	return math.Round(value*1000000) / 1000000
}

func mergeMap(base map[string]any, override map[string]any) map[string]any {
	out := map[string]any{}
	for key, value := range base {
		out[key] = value
	}
	for key, value := range override {
		out[key] = value
	}
	return out
}
