package runner

import (
	"errors"
	"strings"
	"time"

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

type taskRecordDetails struct {
	RequestID          string
	ResolvedModel      string
	Usage              map[string]any
	Metrics            map[string]any
	BillingSummary     map[string]any
	FinalChargeAmount  float64
	ResponseStartedAt  time.Time
	ResponseFinishedAt time.Time
	ResponseDurationMS int64
}

func buildSuccessRecord(task store.GatewayTask, user *auth.User, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, billings []any, simulated bool) taskRecordDetails {
	usage := usageToMap(response.Usage)
	metrics := taskMetrics(task, user, body, candidate, response, simulated)
	summary := summarizeBillings(billings, simulated)
	finalAmount := floatFromAny(summary["totalAmount"])
	return taskRecordDetails{
		RequestID:          response.RequestID,
		ResolvedModel:      candidate.ModelName,
		Usage:              usage,
		Metrics:            metrics,
		BillingSummary:     summary,
		FinalChargeAmount:  finalAmount,
		ResponseStartedAt:  response.ResponseStartedAt,
		ResponseFinishedAt: response.ResponseFinishedAt,
		ResponseDurationMS: response.ResponseDurationMS,
	}
}

func taskMetrics(task store.GatewayTask, user *auth.User, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool) map[string]any {
	metrics := attemptMetrics(candidate, 0, simulated)
	for key, value := range map[string]any{
		"kind":            task.Kind,
		"runMode":         task.RunMode,
		"requestedModel":  task.Model,
		"resolvedModel":   candidate.ModelName,
		"modelAlias":      candidate.ModelAlias,
		"providerModel":   candidate.ProviderModelName,
		"canonicalModel":  candidate.CanonicalModelKey,
		"modelType":       candidate.ModelType,
		"provider":        candidate.Provider,
		"platformId":      candidate.PlatformID,
		"platformName":    candidate.PlatformName,
		"platformModelId": candidate.PlatformModelID,
		"clientId":        candidate.ClientID,
		"queueKey":        candidate.QueueKey,
		"requestId":       response.RequestID,
		"simulated":       simulated,
	} {
		metrics[key] = value
	}
	if user != nil {
		metrics["apiKeyId"] = user.APIKeyID
		metrics["apiKeyName"] = user.APIKeyName
		metrics["apiKeyPrefix"] = user.APIKeyPrefix
	}
	if response.ResponseDurationMS > 0 {
		metrics["responseDurationMs"] = response.ResponseDurationMS
	}
	if response.UpstreamProtocol != "" {
		metrics["upstreamProtocol"] = response.UpstreamProtocol
		metrics["upstreamEndpoint"] = response.UpstreamEndpoint
		metrics["responseConverted"] = response.ResponseConverted
		metrics["publicResponseId"] = response.PublicResponseID
		metrics["upstreamResponseId"] = response.UpstreamResponseID
		metrics["parentResponseId"] = response.ParentResponseID
		metrics["responseChainDepth"] = response.ResponseChainDepth
	}
	switch task.Kind {
	case "chat.completions", "responses":
		metrics["stream"] = boolFromMap(body, "stream")
		metrics["messageCount"] = messageCount(body)
		copyIfPresent(metrics, body, "temperature")
		copyIfPresent(metrics, body, "max_tokens")
		copyIfPresent(metrics, body, "max_output_tokens")
	case "images.generations", "images.edits":
		metrics["imageCount"] = requestedCount(body)
		metrics["outputImageCount"] = outputDataCount(response.Result)
		metrics["inputImageCount"] = imageInputCount(body)
		metrics["hasMask"] = stringFromMap(body, "mask") != ""
		copyIfPresent(metrics, body, "size")
		copyIfPresent(metrics, body, "quality")
		copyIfPresent(metrics, body, "style")
	case "videos.generations":
		metrics["hasReferenceImage"] = imageInputCount(body) > 0
		metrics["hasReferenceVideo"] = hasAnyString(body, "video", "video_url", "videoUrl", "reference_video", "referenceVideo") || hasVideoContent(body)
		copyIfPresent(metrics, body, "duration")
		copyIfPresent(metrics, body, "resolution")
		copyIfPresent(metrics, body, "size")
		copyIfPresent(metrics, body, "aspect_ratio")
		copyIfPresent(metrics, body, "aspectRatio")
		copyIfPresent(metrics, body, "fps")
	}
	return metrics
}

func attemptMetrics(candidate store.RuntimeModelCandidate, attemptNo int, simulated bool) map[string]any {
	metrics := map[string]any{
		"resolvedModel":    candidate.ModelName,
		"modelName":        candidate.ModelName,
		"modelAlias":       candidate.ModelAlias,
		"providerModel":    candidate.ProviderModelName,
		"canonicalModel":   candidate.CanonicalModelKey,
		"modelType":        candidate.ModelType,
		"provider":         candidate.Provider,
		"platformId":       candidate.PlatformID,
		"platformKey":      candidate.PlatformKey,
		"platformName":     candidate.PlatformName,
		"platformModelId":  candidate.PlatformModelID,
		"clientId":         candidate.ClientID,
		"queueKey":         candidate.QueueKey,
		"platformPriority": candidate.PlatformPriority,
		"currentPriority":  candidate.PlatformPriority,
		"loadRatio":        candidate.LoadRatio,
		"loadAvoided":      candidate.LoadAvoided,
		"fullReasons":      candidate.FullReasons,
		"selectionReason":  candidate.SelectionReason,
		"simulated":        simulated,
	}
	if candidate.ResponseProtocol != "" {
		metrics["upstreamProtocol"] = candidate.ResponseProtocol
		if candidate.ResponseProtocol == clients.ProtocolOpenAIResponses {
			metrics["upstreamEndpoint"] = "/responses"
			metrics["responseConverted"] = false
		} else if candidate.ResponseProtocol == clients.ProtocolOpenAIChatCompletions {
			metrics["upstreamEndpoint"] = "/chat/completions"
			metrics["responseConverted"] = true
		}
	}
	metrics["loadMetrics"] = map[string]any{
		"rpm": map[string]any{
			"current": candidate.LoadMetrics.RPMCurrent,
			"limit":   candidate.LoadMetrics.RPMLimit,
			"ratio":   candidate.LoadMetrics.RPMRatio,
		},
		"tpm": map[string]any{
			"current": candidate.LoadMetrics.TPMCurrent,
			"limit":   candidate.LoadMetrics.TPMLimit,
			"ratio":   candidate.LoadMetrics.TPMRatio,
		},
		"concurrent": map[string]any{
			"current":     candidate.LoadMetrics.ConcurrentCurrent,
			"limit":       candidate.LoadMetrics.ConcurrentLimit,
			"ratio":       candidate.LoadMetrics.ConcurrentRatio,
			"softCurrent": candidate.LoadMetrics.SoftCurrent,
			"softRatio":   candidate.LoadMetrics.SoftRatio,
		},
		"queued": candidate.LoadMetrics.QueuedCount,
	}
	if candidate.CacheAffinity.Key != "" {
		metrics["cacheAffinityKey"] = candidate.CacheAffinity.Key
		metrics["cacheAdjustedPriority"] = candidate.CacheAffinity.AdjustedPriority
		metrics["cacheAffinitySamples"] = candidate.CacheAffinity.RequestCount
		metrics["cacheAffinityScore"] = candidate.CacheAffinity.Score
		metrics["cacheAffinityConfidence"] = candidate.CacheAffinity.Confidence
		metrics["cacheAffinityHitRatio"] = candidate.CacheAffinity.EMAHitRatio
		metrics["cacheAffinityEMAHitRatio"] = candidate.CacheAffinity.EMAHitRatio
		metrics["cacheAffinityLastHitRatio"] = candidate.CacheAffinity.LastHitRatio
		metrics["cacheAffinityCachedInputTokens"] = candidate.CacheAffinity.CachedInputTokens
		metrics["cacheAffinityBoost"] = candidate.CacheAffinity.Boost
		metrics["cacheAffinityApplied"] = candidate.CacheAffinity.Applied
		metrics["cacheAffinityMatched"] = candidate.CacheAffinity.Applied
		metrics["cacheAffinityCandidateCount"] = candidate.CacheAffinity.CandidateCount
		if candidate.CacheAffinity.MatchedPrefixDepth > 0 {
			metrics["cacheAffinityMatchedPrefixDepth"] = candidate.CacheAffinity.MatchedPrefixDepth
		}
		if candidate.CacheAffinity.OverrideReason != "" {
			metrics["cacheAffinityOverrideReason"] = candidate.CacheAffinity.OverrideReason
		}
	}
	if attemptNo > 0 {
		metrics["attempt"] = attemptNo
	}
	return metrics
}

func usageToMap(usage clients.Usage) map[string]any {
	out := map[string]any{}
	if usage.InputTokens > 0 {
		out["inputTokens"] = usage.InputTokens
		out["promptTokens"] = usage.InputTokens
	}
	if usage.CachedInputTokensKnown || usage.CachedInputTokens > 0 {
		out["cachedInputTokens"] = usage.CachedInputTokens
		out["cachedPromptTokens"] = usage.CachedInputTokens
		out["cachedInputTokensKnown"] = usage.CachedInputTokensKnown
	}
	if usage.OutputTokens > 0 {
		out["outputTokens"] = usage.OutputTokens
		out["completionTokens"] = usage.OutputTokens
	}
	if usage.TotalTokens > 0 {
		out["totalTokens"] = usage.TotalTokens
	}
	return out
}

func summarizeBillings(billings []any, simulated bool) map[string]any {
	amountByCurrency := map[string]float64{}
	for _, raw := range billings {
		line, _ := raw.(map[string]any)
		if line == nil {
			continue
		}
		currency := strings.TrimSpace(stringFromAny(line["currency"]))
		if currency == "" {
			currency = "resource"
		}
		amountByCurrency[currency] = roundPrice(amountByCurrency[currency] + floatFromAny(line["amount"]))
	}
	currency := ""
	totalAmount := 0.0
	for key, amount := range amountByCurrency {
		if currency == "" {
			currency = key
		} else if currency != key {
			currency = "mixed"
		}
		totalAmount += amount
	}
	if currency == "" {
		currency = "resource"
	}
	totalAmount = roundPrice(totalAmount)
	return map[string]any{
		"lineCount":        len(billings),
		"totalAmount":      totalAmount,
		"amountByCurrency": amountByCurrency,
		"currency":         currency,
		"simulated":        simulated,
		"finalCharge": map[string]any{
			"amount":    totalAmount,
			"currency":  currency,
			"simulated": simulated,
		},
	}
}

func failureMetrics(err error, simulated bool) (string, map[string]any, time.Time, time.Time, int64) {
	meta := clients.ErrorResponseMetadata(err)
	metrics := map[string]any{
		"simulated": simulated,
	}
	retryable := clients.IsRetryable(err)
	if err != nil {
		info := failureInfoFromError(err)
		metrics["error"] = err.Error()
		metrics["errorCategory"] = info.Category
		metrics["retryable"] = retryable
		if details := clients.ErrorDetails(err); len(details) > 0 {
			metrics["errorDetails"] = details
		}
		if detail := rateLimitFailureDetail(err); len(detail) > 0 {
			metrics["rateLimit"] = detail
		}
		if detail := store.ModelCandidateErrorDetails(err); len(detail) > 0 {
			metrics["modelCandidate"] = detail
		}
	}
	if meta.StatusCode > 0 {
		metrics["statusCode"] = meta.StatusCode
	}
	if meta.RequestID != "" {
		metrics["requestId"] = meta.RequestID
	}
	if meta.ResponseDurationMS > 0 {
		metrics["responseDurationMs"] = meta.ResponseDurationMS
	}
	if err != nil {
		metrics["trace"] = []any{failureTraceEntry(err, retryable)}
	}
	return meta.RequestID, metrics, meta.ResponseStartedAt, meta.ResponseFinishedAt, meta.ResponseDurationMS
}

func buildFailureResult(code string, message string, requestID string, err error) map[string]any {
	errorPayload := map[string]any{
		"code":    code,
		"message": message,
	}
	if requestID != "" {
		errorPayload["requestId"] = requestID
	}
	if details := clients.ErrorDetails(err); len(details) > 0 {
		errorPayload["details"] = details
	}
	if detail := rateLimitFailureDetail(err); len(detail) > 0 {
		errorPayload["rateLimit"] = detail
	}
	if detail := store.ModelCandidateErrorDetails(err); len(detail) > 0 {
		errorPayload["modelCandidate"] = detail
	}
	return map[string]any{"error": errorPayload}
}

func rateLimitFailureDetail(err error) map[string]any {
	var limitErr *store.RateLimitExceededError
	if !errors.As(err, &limitErr) {
		return nil
	}
	detail := map[string]any{
		"scopeType":     limitErr.ScopeType,
		"scopeKey":      limitErr.ScopeKey,
		"scopeName":     limitErr.ScopeName,
		"metric":        limitErr.Metric,
		"limit":         limitErr.Limit,
		"amount":        limitErr.Amount,
		"current":       limitErr.Current,
		"used":          limitErr.Used,
		"reserved":      limitErr.Reserved,
		"projected":     limitErr.Projected,
		"windowSeconds": limitErr.WindowSeconds,
		"retryable":     limitErr.Retryable,
		"exceeded": map[string]any{
			"metric":    limitErr.Metric,
			"current":   limitErr.Current,
			"amount":    limitErr.Amount,
			"projected": limitErr.Projected,
			"limit":     limitErr.Limit,
		},
	}
	if limitErr.RetryAfter > 0 {
		detail["retryAfterMs"] = limitErr.RetryAfter.Milliseconds()
	}
	if !limitErr.ResetAt.IsZero() {
		detail["resetAt"] = limitErr.ResetAt.UTC().Format(time.RFC3339Nano)
	}
	if len(limitErr.ScopeMetadata) > 0 {
		detail["scopeMetadata"] = limitErr.ScopeMetadata
	}
	if len(limitErr.Policy) > 0 {
		detail["rateLimitPolicy"] = limitErr.Policy
	}
	return detail
}

func mergeMetrics(values ...map[string]any) map[string]any {
	out := map[string]any{}
	for _, value := range values {
		for key, item := range value {
			out[key] = item
		}
	}
	return out
}

func summarizeAttempts(attempts []store.TaskAttempt) []map[string]any {
	items := make([]map[string]any, 0, len(attempts))
	for _, attempt := range attempts {
		item := map[string]any{
			"attempt":            attempt.AttemptNo,
			"status":             attempt.Status,
			"platformId":         attempt.PlatformID,
			"platformName":       attempt.PlatformName,
			"provider":           attempt.Provider,
			"platformModelId":    attempt.PlatformModelID,
			"modelName":          attempt.ModelName,
			"providerModelName":  attempt.ProviderModelName,
			"modelAlias":         attempt.ModelAlias,
			"modelType":          attempt.ModelType,
			"clientId":           attempt.ClientID,
			"queueKey":           attempt.QueueKey,
			"requestId":          attempt.RequestID,
			"retryable":          attempt.Retryable,
			"simulated":          attempt.Simulated,
			"statusCode":         attempt.StatusCode,
			"errorCode":          attempt.ErrorCode,
			"errorMessage":       attempt.ErrorMessage,
			"responseDurationMs": attempt.ResponseDurationMS,
			"startedAt":          attempt.StartedAt,
			"finishedAt":         attempt.FinishedAt,
		}
		if trace, ok := attempt.Metrics["trace"]; ok {
			item["trace"] = trace
		}
		if preprocessing, ok := attempt.Metrics["parameterPreprocessingSummary"]; ok {
			item["parameterPreprocessingSummary"] = preprocessing
		}
		items = append(items, item)
	}
	return items
}

func messageCount(body map[string]any) int {
	messages, _ := body["messages"].([]any)
	return len(messages)
}

func requestedCount(body map[string]any) int {
	count := int(floatFromAny(body["n"]))
	if count <= 0 {
		return 1
	}
	return count
}

func outputDataCount(result map[string]any) int {
	data, _ := result["data"].([]any)
	return len(data)
}

func imageInputCount(body map[string]any) int {
	count := 0
	for _, key := range []string{"image", "image_url", "imageUrl"} {
		if stringFromMap(body, key) != "" {
			count++
		}
	}
	for _, key := range []string{"image_urls", "imageUrls", "images"} {
		if values, ok := body[key].([]any); ok {
			count += len(values)
		}
	}
	for _, item := range contentItems(body["content"]) {
		if isImageContent(item) {
			count++
		}
	}
	return count
}

func hasVideoContent(body map[string]any) bool {
	for _, item := range contentItems(body["content"]) {
		if isVideoContent(item) {
			return true
		}
	}
	return false
}

func hasAnyString(body map[string]any, keys ...string) bool {
	for _, key := range keys {
		if stringFromMap(body, key) != "" {
			return true
		}
	}
	return false
}

func copyIfPresent(target map[string]any, body map[string]any, key string) {
	if value, ok := body[key]; ok && value != nil {
		target[key] = value
	}
}
