package runner

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net"
	"net/http"
	"net/textproto"
	"net/url"
	"sort"
	"strings"
	"sync"
	"time"

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

const defaultServerMainOpenAPIUploadURL = "http://127.0.0.1:3001/v1/files/upload"
const maxGeneratedAssetFetchBytes = 256 << 20

type FileUploadPayload struct {
	ContentType string
	FileName    string
	Scene       string
	Source      string
	Bytes       []byte
}

type generatedAssetUploadPolicy struct {
	UploadInlineMedia   bool
	UploadURLMedia      bool
	PreserveInlineMedia bool
	LocalizeInlineMedia bool
}

type generatedAssetDecision struct {
	Inline    *generatedInlineAsset
	URL       *generatedURLAsset
	StripKeys []string
}

type generatedAssetUploadResult struct {
	upload      map[string]any
	contentType string
	kind        string
	strategy    string
}

type generatedInlineAsset struct {
	Bytes       []byte
	ContentType string
	Kind        string
	SourceKey   string
}

type generatedURLAsset struct {
	URL         string
	ContentType string
	Kind        string
	SourceKey   string
}

func defaultGeneratedAssetUploadPolicy() generatedAssetUploadPolicy {
	return generatedAssetUploadPolicy{
		UploadInlineMedia: true,
		UploadURLMedia:    false,
	}
}

func (s *Service) acquireMediaMaterializationSlot(ctx context.Context) (func(), error) {
	if s.mediaResultSlots == nil {
		return func() {}, nil
	}
	select {
	case s.mediaResultSlots <- struct{}{}:
		var once sync.Once
		return func() {
			once.Do(func() {
				<-s.mediaResultSlots
			})
		}, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	}
}

func (s *Service) acquireMediaResultSlot(ctx context.Context, result map[string]any) (func(), error) {
	if s.mediaResultSlots == nil ||
		(!TaskResultHasInlineBinary(result) && !generatedRawValueHasInlineMedia(result["raw"], "", nil)) {
		return func() {}, nil
	}
	return s.acquireMediaMaterializationSlot(ctx)
}

func mediaTaskNeedsPreUpstreamMaterializationSlot(modelType string) bool {
	switch canonicalModelType(modelType) {
	case "image_generate", "image_edit", "audio_generate", "text_to_speech", "voice_clone", "omni":
		return true
	default:
		return false
	}
}

func (s *Service) uploadGeneratedAssets(ctx context.Context, taskID string, taskKind string, acceptanceRunID string, result map[string]any) (map[string]any, error) {
	data, _ := result["data"].([]any)
	rawNeedsUpload := generatedRawValueHasInlineMedia(result["raw"], "", nil)
	hasInlineBinary := TaskResultHasInlineBinary(result)
	if len(data) == 0 && !rawNeedsUpload && !hasInlineBinary {
		redactGeneratedResultRawData(result)
		return result, nil
	}
	policy, err := s.generatedAssetUploadPolicy(ctx)
	if err != nil {
		return nil, &clients.ClientError{Code: "upload_config_failed", Message: err.Error(), Retryable: true}
	}
	// Acceptance runs must prove that a final URL returned by the protocol
	// emulator can be persisted and consumed outside the cluster. Keep this
	// override scoped to the authenticated run so normal production tasks retain
	// the configured file-storage policy.
	policy = generatedAssetUploadPolicyForAcceptanceRun(policy, acceptanceRunID)
	acceptanceEmulatorBaseURL := ""
	if strings.TrimSpace(acceptanceRunID) != "" {
		acceptanceEmulatorBaseURL, _, err = s.store.AcceptanceCandidateOverride(ctx, acceptanceRunID)
		if err != nil {
			return nil, &clients.ClientError{Code: "acceptance_run_inactive", Message: err.Error(), Retryable: false}
		}
	}
	if len(data) == 0 && !rawNeedsUpload {
		return s.finalizeGeneratedAssets(ctx, taskID, taskKind, result, policy, nil, false, 0)
	}
	// Topaz download URLs are short-lived. Persist them before the task can be
	// marked succeeded even when the global policy normally keeps URL media.
	if taskKind == "videos.upscales" {
		policy.UploadURLMedia = true
	}
	decisions := make([]generatedAssetDecision, len(data))
	needsUpload := false
	changed := false
	for index, rawItem := range data {
		item, _ := rawItem.(map[string]any)
		if item == nil {
			continue
		}
		decision, err := generatedAssetDecisionForItem(taskKind, item, policy)
		if err != nil {
			return nil, err
		}
		decisions[index] = decision
		if _, mediaURL := mediaURLSourceFromItem(item); mediaURL != "" && decision.URL == nil {
			s.observeResultStorage("upstream_url")
		}
		if decision.Inline != nil || decision.URL != nil {
			needsUpload = true
		}
		if len(decision.StripKeys) > 0 {
			changed = true
		}
	}
	rawNeedsUpload = rawNeedsUpload && policy.UploadInlineMedia
	if !needsUpload && !changed && !rawNeedsUpload {
		return s.finalizeGeneratedAssets(ctx, taskID, taskKind, result, policy, nil, false, 0)
	}
	var channels []store.FileStorageChannel
	channelsLoaded := false
	if needsUpload || rawNeedsUpload {
		channels, err = s.activeFileStorageChannels(ctx, store.FileStorageSceneImageResult)
		if err != nil {
			return nil, &clients.ClientError{Code: "storage_write_failed", Message: "object storage configuration is unavailable", Retryable: true}
		}
		channelsLoaded = true
	}
	next := map[string]any{}
	for key, value := range result {
		next[key] = value
	}
	nextData := make([]any, 0, len(data))
	uploadCache := make(map[string]generatedAssetUploadResult)
	for index, rawItem := range data {
		item, _ := rawItem.(map[string]any)
		if item == nil {
			nextData = append(nextData, rawItem)
			continue
		}
		decision := decisions[index]
		merged := map[string]any{}
		for key, value := range item {
			merged[key] = value
		}
		for _, key := range decision.StripKeys {
			delete(merged, key)
		}
		if decision.Inline != nil || decision.URL != nil {
			var upload map[string]any
			var sourceKey string
			var strategy string
			var kind string
			var contentType string
			var err error
			if decision.Inline != nil {
				cacheKey := generatedInlineAssetCacheKey(decision.Inline)
				if cached, ok := uploadCache[cacheKey]; ok {
					upload, contentType, kind, strategy = cached.upload, cached.contentType, cached.kind, cached.strategy
				} else {
					upload, contentType, kind, strategy, err = s.uploadGeneratedAsset(ctx, taskID, decision.Inline, index, channels)
					if err == nil {
						uploadCache[cacheKey] = generatedAssetUploadResult{upload: upload, contentType: contentType, kind: kind, strategy: strategy}
					}
				}
				sourceKey = decision.Inline.SourceKey
			} else {
				upload, contentType, kind, strategy, err = s.uploadGeneratedURLAsset(ctx, taskID, decision.URL, index, channels, acceptanceEmulatorBaseURL)
				sourceKey = decision.URL.SourceKey
			}
			if err != nil {
				return nil, err
			}
			merged["upload"] = upload
			merged["assetStorage"] = map[string]any{
				"scene":    store.FileStorageSceneImageResult,
				"source":   sourceKey,
				"strategy": strategy,
			}
			if contentType != "" {
				merged["assetStorage"].(map[string]any)["contentType"] = contentType
			}
			if urlValue := stringFromAny(upload["url"]); urlValue != "" {
				merged["url"] = urlValue
				if kind == "video" {
					merged["video_url"] = urlValue
				}
				if kind == "image" {
					merged["image_url"] = urlValue
				}
				if kind == "audio" {
					merged["audio_url"] = urlValue
				}
			}
			if kind != "" && stringFromAny(merged["type"]) == "" {
				merged["type"] = kind
			}
			if contentType != "" && stringFromAny(merged["mime_type"]) == "" {
				merged["mime_type"] = contentType
			}
		}
		nextData = append(nextData, merged)
	}
	if len(data) > 0 {
		next["data"] = nextData
	}
	if rawNeedsUpload {
		rawIndex := len(nextData)
		raw, rawChanged, err := s.uploadGeneratedRawMediaValue(ctx, taskID, taskKind, next["raw"], "", nil, policy, channels, &rawIndex)
		if err != nil {
			return nil, err
		}
		if rawChanged {
			next["raw"] = raw
		}
	}
	return s.finalizeGeneratedAssets(ctx, taskID, taskKind, next, policy, channels, channelsLoaded, len(nextData))
}

func generatedInlineAssetCacheKey(asset *generatedInlineAsset) string {
	if asset == nil {
		return ""
	}
	digest := sha256.Sum256(asset.Bytes)
	contentType := resolvedGeneratedAssetContentType(asset.ContentType, asset.Kind, asset.Bytes)
	return hex.EncodeToString(digest[:]) + ":" + contentType
}

func generatedAssetUploadPolicyForAcceptanceRun(policy generatedAssetUploadPolicy, acceptanceRunID string) generatedAssetUploadPolicy {
	if strings.TrimSpace(acceptanceRunID) != "" {
		policy.UploadURLMedia = true
	}
	return policy
}

func (s *Service) finalizeGeneratedAssets(
	ctx context.Context,
	taskID string,
	taskKind string,
	result map[string]any,
	policy generatedAssetUploadPolicy,
	channels []store.FileStorageChannel,
	channelsLoaded bool,
	startIndex int,
) (map[string]any, error) {
	redactGeneratedResultRawData(result)
	if !TaskResultHasInlineBinary(result) {
		return canonicalStoredResultURLs(result), nil
	}
	next := result
	if policy.UploadInlineMedia {
		var err error
		if !channelsLoaded {
			channels, err = s.activeFileStorageChannels(ctx, store.FileStorageSceneImageResult)
			if err != nil {
				return nil, &clients.ClientError{Code: "upload_config_failed", Message: err.Error(), Retryable: true}
			}
		}
		index := startIndex
		uploaded, changed, err := s.uploadGeneratedRawMediaValue(ctx, taskID, taskKind, next, "", nil, policy, channels, &index)
		if err != nil {
			return nil, err
		}
		if changed {
			mapped, ok := uploaded.(map[string]any)
			if !ok {
				return nil, &clients.ClientError{
					Code:       "result_binary_not_materialized",
					Message:    "generated result is not a JSON object",
					StatusCode: 500,
					Retryable:  false,
				}
			}
			next = mapped
		}
	}
	if !TaskResultHasInlineBinary(next) {
		return canonicalStoredResultURLs(next), nil
	}
	diagnostics := taskResultInlineBinaryDiagnostics(next)
	if s.logger != nil {
		s.logger.Error("generated result still contains inline binary after object storage materialization",
			"taskID", taskID,
			"remainingInlineBinary", diagnostics,
		)
	}
	return nil, &clients.ClientError{
		Code:       "storage_write_failed",
		Message:    "generated binary result could not be written to object storage",
		Details:    map[string]any{"scope": "gateway_result_storage", "remainingInlineBinary": diagnostics},
		StatusCode: http.StatusServiceUnavailable,
		Retryable:  true,
	}
}

func canonicalStoredResultURLs(result map[string]any) map[string]any {
	next, changed := migrateStoredResultURLValue(result, 0)
	if !changed {
		return result
	}
	mapped, ok := next.(map[string]any)
	if !ok {
		return result
	}
	return mapped
}

func generatedRawValueHasInlineMedia(value any, key string, siblings map[string]any) bool {
	switch typed := value.(type) {
	case map[string]any:
		for childKey, childValue := range typed {
			if generatedRawValueHasInlineMedia(childValue, childKey, typed) {
				return true
			}
		}
	case []any:
		for _, item := range typed {
			if generatedRawValueHasInlineMedia(item, key, siblings) {
				return true
			}
		}
	case string:
		_, ok := generatedRawInlineMediaAsset(key, typed, siblings, "")
		return ok
	}
	return false
}

func (s *Service) uploadGeneratedRawMediaValue(ctx context.Context, taskID string, taskKind string, value any, key string, siblings map[string]any, policy generatedAssetUploadPolicy, channels []store.FileStorageChannel, index *int) (any, bool, error) {
	switch typed := value.(type) {
	case map[string]any:
		if payload, contentType, ok := localBufferObjectBytes(typed); ok {
			return s.uploadGeneratedBinaryValue(ctx, taskID, taskKind, payload, contentType, firstNonEmptyString(key, "buffer"), siblings, channels, index)
		}
		next := make(map[string]any, len(typed))
		changed := false
		for childKey, childValue := range typed {
			uploaded, childChanged, err := s.uploadGeneratedRawMediaValue(ctx, taskID, taskKind, childValue, childKey, typed, policy, channels, index)
			if err != nil {
				return nil, false, err
			}
			next[childKey] = uploaded
			if childChanged {
				changed = true
			}
		}
		if changed {
			return next, true, nil
		}
		return value, false, nil
	case []any:
		if localBinaryKey(key) {
			if payload, ok := bytesFromNumberArray(typed); ok {
				return s.uploadGeneratedBinaryValue(ctx, taskID, taskKind, payload, mediaContentTypeFromItem(siblings), firstNonEmptyString(key, "bytes"), siblings, channels, index)
			}
		}
		next := make([]any, len(typed))
		changed := false
		for itemIndex, item := range typed {
			uploaded, itemChanged, err := s.uploadGeneratedRawMediaValue(ctx, taskID, taskKind, item, key, siblings, policy, channels, index)
			if err != nil {
				return nil, false, err
			}
			next[itemIndex] = uploaded
			if itemChanged {
				changed = true
			}
		}
		if changed {
			return next, true, nil
		}
		return value, false, nil
	case []byte:
		if len(typed) == 0 {
			return value, false, nil
		}
		return s.uploadGeneratedBinaryValue(ctx, taskID, taskKind, append([]byte(nil), typed...), mediaContentTypeFromItem(siblings), firstNonEmptyString(key, "bytes"), siblings, channels, index)
	case string:
		asset, ok := generatedRawInlineMediaAsset(key, typed, siblings, taskKind)
		if !ok {
			return value, false, nil
		}
		upload, contentType, kind, strategy, err := s.uploadGeneratedAsset(ctx, taskID, asset, *index, channels)
		if err != nil {
			return nil, false, err
		}
		*index = *index + 1
		return generatedRawMediaReference(asset, upload, contentType, kind, strategy), true, nil
	default:
		return value, false, nil
	}
}

func (s *Service) uploadGeneratedBinaryValue(ctx context.Context, taskID string, taskKind string, payload []byte, contentType string, sourceKey string, siblings map[string]any, channels []store.FileStorageChannel, index *int) (any, bool, error) {
	contentType = firstNonEmptyString(contentType, defaultContentTypeForRawMediaKey(sourceKey))
	asset := &generatedInlineAsset{
		Bytes:       payload,
		ContentType: contentType,
		Kind:        mediaKindForAsset(taskKind, siblings, sourceKey, contentType),
		SourceKey:   sourceKey,
	}
	upload, resolvedContentType, kind, strategy, err := s.uploadGeneratedAsset(ctx, taskID, asset, *index, channels)
	if err != nil {
		return nil, false, err
	}
	*index = *index + 1
	return generatedRawMediaReference(asset, upload, resolvedContentType, kind, strategy), true, nil
}

func generatedRawInlineMediaAsset(key string, value string, siblings map[string]any, taskKind string) (*generatedInlineAsset, bool) {
	payload, contentType, _, ok := localBinaryStringBytes(key, value, siblings)
	if !ok {
		contentType = firstNonEmptyString(mediaContentTypeFromItem(siblings), defaultContentTypeForRawMediaKey(key))
		if !generatedContentTypeIsMedia(contentType) && !generatedContentTypeIsDocument(contentType) {
			return nil, false
		}
		var err error
		payload, _, ok, err = inlineMediaPayload(value, false)
		if err != nil || !ok || len(payload) == 0 {
			return nil, false
		}
	}
	contentType = firstNonEmptyString(contentType, detectGeneratedAssetContentType(payload))
	kind := mediaKindForAsset(taskKind, siblings, key, contentType)
	return &generatedInlineAsset{
		Bytes:       payload,
		ContentType: contentType,
		Kind:        kind,
		SourceKey:   key,
	}, true
}

func generatedRawMediaReference(asset *generatedInlineAsset, upload map[string]any, contentType string, kind string, strategy string) map[string]any {
	urlValue := stringFromAny(upload["url"])
	out := map[string]any{
		"upload": upload,
		"assetStorage": map[string]any{
			"scene":       store.FileStorageSceneImageResult,
			"source":      asset.SourceKey,
			"strategy":    strategy,
			"contentType": contentType,
		},
	}
	if urlValue != "" {
		out["url"] = urlValue
	}
	if kind != "" {
		out["type"] = kind
	}
	if contentType != "" {
		out["mime_type"] = contentType
	}
	return out
}

func redactGeneratedResultRawData(result map[string]any) bool {
	if result == nil {
		return false
	}
	changed := false
	for _, key := range []string{"raw_data", "rawData"} {
		value, ok := result[key]
		if !ok || value == nil {
			continue
		}
		next, nextChanged := redactGeneratedRawDataValue(value, key, nil)
		if nextChanged {
			result[key] = next
			changed = true
		}
	}
	return changed
}

func redactGeneratedRawDataValue(value any, key string, siblings map[string]any) (any, bool) {
	switch typed := value.(type) {
	case map[string]any:
		next := make(map[string]any, len(typed))
		changed := false
		for childKey, childValue := range typed {
			redacted, childChanged := redactGeneratedRawDataValue(childValue, childKey, typed)
			next[childKey] = redacted
			if childChanged {
				changed = true
			}
		}
		if changed {
			return next, true
		}
		return value, false
	case []any:
		next := make([]any, len(typed))
		changed := false
		for index, item := range typed {
			redacted, itemChanged := redactGeneratedRawDataValue(item, key, siblings)
			next[index] = redacted
			if itemChanged {
				changed = true
			}
		}
		if changed {
			return next, true
		}
		return value, false
	case string:
		redacted, ok := generatedRawDataMediaRedaction(key, typed, siblings)
		if ok {
			return redacted, true
		}
		return value, false
	default:
		return value, false
	}
}

func generatedRawDataMediaRedaction(key string, value string, siblings map[string]any) (map[string]any, bool) {
	raw := strings.TrimSpace(value)
	if raw == "" {
		return nil, false
	}
	contentType := firstNonEmptyString(mediaContentTypeFromItem(siblings), defaultContentTypeForRawMediaKey(key))
	if strings.HasPrefix(strings.ToLower(raw), "data:") {
		declared, encoded, ok, err := parseBase64DataURL(raw)
		if err != nil || !ok || !generatedContentTypeIsMedia(declared) {
			return nil, false
		}
		payload, err := decodeBase64Payload(encoded)
		if err != nil {
			return nil, false
		}
		return generatedRawDataRedaction("data_url", declared, payload, raw), true
	}
	if len(raw) < 128 {
		return nil, false
	}
	keyLooksLikeMediaPayload := generatedRawDataMediaPayloadKey(key)
	if !keyLooksLikeMediaPayload && !generatedContentTypeIsMedia(contentType) {
		return nil, false
	}
	if keyLooksLikeMediaPayload && looksHexMediaPayload(raw) {
		normalized := removeASCIIWhitespace(raw)
		payload, err := hex.DecodeString(normalized)
		if err == nil && len(payload) > 0 {
			return generatedRawDataRedaction("hex", contentType, payload, raw), true
		}
	}
	if payload, err := decodeBase64Payload(raw); err == nil && len(payload) > 0 {
		return generatedRawDataRedaction("base64", contentType, payload, raw), true
	}
	if keyLooksLikeMediaPayload && len(raw) > 2048 {
		return generatedRawDataRedaction("unknown", contentType, nil, raw), true
	}
	return nil, false
}

func generatedRawDataMediaPayloadKey(key string) bool {
	normalized := strings.ToLower(strings.TrimSpace(key))
	normalized = strings.ReplaceAll(normalized, "-", "_")
	return normalized == "audio" ||
		normalized == "image" ||
		normalized == "video" ||
		normalized == "b64_json" ||
		normalized == "base64" ||
		normalized == "b64" ||
		normalized == "binary_data_base64" ||
		strings.Contains(normalized, "base64") ||
		strings.Contains(normalized, "_b64") ||
		strings.Contains(normalized, "audio_data") ||
		strings.Contains(normalized, "image_data") ||
		strings.Contains(normalized, "video_data")
}

func defaultContentTypeForRawMediaKey(key string) string {
	normalized := strings.ToLower(strings.TrimSpace(key))
	if strings.Contains(normalized, "audio") {
		return "audio/mpeg"
	}
	if strings.Contains(normalized, "video") {
		return "video/mp4"
	}
	if strings.Contains(normalized, "image") || strings.Contains(normalized, "b64_json") {
		return "image/png"
	}
	return ""
}

func generatedRawDataRedaction(encoding string, contentType string, payload []byte, raw string) map[string]any {
	out := map[string]any{
		"redacted": true,
		"reason":   "inline_media_payload",
		"encoding": encoding,
	}
	if contentType = normalizeGeneratedContentType(contentType); contentType != "" {
		out["contentType"] = contentType
	}
	if len(payload) > 0 {
		digest := sha256.Sum256(payload)
		out["size"] = len(payload)
		out["sha256"] = hex.EncodeToString(digest[:])
	} else {
		out["size"] = len(raw)
	}
	return out
}

func looksHexMediaPayload(value string) bool {
	normalized := removeASCIIWhitespace(value)
	if len(normalized) < 128 || len(normalized)%2 != 0 {
		return false
	}
	for _, item := range normalized {
		if (item >= '0' && item <= '9') || (item >= 'a' && item <= 'f') || (item >= 'A' && item <= 'F') {
			continue
		}
		return false
	}
	return true
}

func removeASCIIWhitespace(value string) string {
	return strings.Map(func(r rune) rune {
		switch r {
		case '\n', '\r', '\t', ' ':
			return -1
		default:
			return r
		}
	}, value)
}

func (s *Service) generatedAssetUploadPolicy(ctx context.Context) (generatedAssetUploadPolicy, error) {
	if s.store == nil {
		return defaultGeneratedAssetUploadPolicy(), nil
	}
	settings, err := s.store.GetFileStorageSettings(ctx)
	if err != nil {
		if store.IsUndefinedDatabaseObject(err) {
			return defaultGeneratedAssetUploadPolicy(), nil
		}
		return generatedAssetUploadPolicy{}, err
	}
	return generatedAssetUploadPolicyFromName(settings.ResultUploadPolicy), nil
}

func generatedAssetUploadPolicyFromName(policyName string) generatedAssetUploadPolicy {
	policyName = store.NormalizeFileStorageResultUploadPolicy(policyName)
	switch policyName {
	case store.FileStorageResultUploadPolicyUploadAll:
		return generatedAssetUploadPolicy{UploadInlineMedia: true, UploadURLMedia: true}
	default:
		return defaultGeneratedAssetUploadPolicy()
	}
}

func (s *Service) uploadGeneratedAsset(ctx context.Context, taskID string, asset *generatedInlineAsset, index int, channels []store.FileStorageChannel) (map[string]any, string, string, string, error) {
	contentType := resolvedGeneratedAssetContentType(asset.ContentType, asset.Kind, asset.Bytes)
	kind := generatedAssetKindFromContentType(asset.Kind, contentType)
	payload := FileUploadPayload{
		Bytes:       asset.Bytes,
		ContentType: contentType,
		FileName:    generatedAssetFileName(taskID, index, contentType, kind),
		Scene:       store.FileStorageSceneImageResult,
		Source:      "ai-gateway",
	}
	if len(channels) == 0 {
		return nil, "", "", "", &clients.ClientError{Code: "storage_write_failed", Message: "no enabled object storage channel", StatusCode: http.StatusServiceUnavailable, Retryable: true}
	}
	upload, err := s.uploadFileWithFailover(ctx, payload, channels)
	if err == nil {
		s.observeResultStorage("uploaded")
	}
	return upload, contentType, kind, "upload_inline_media", err
}

func (s *Service) uploadGeneratedURLAsset(ctx context.Context, taskID string, asset *generatedURLAsset, index int, channels []store.FileStorageChannel, acceptanceEmulatorBaseURL string) (map[string]any, string, string, string, error) {
	payload, contentType, err := s.readGeneratedURLAsset(ctx, asset, acceptanceEmulatorBaseURL)
	if err != nil {
		return nil, "", "", "", err
	}
	contentType = resolvedGeneratedAssetContentType(firstNonEmptyString(contentType, asset.ContentType), asset.Kind, payload)
	kind := generatedAssetKindFromContentType(asset.Kind, contentType)
	uploadPayload := FileUploadPayload{
		Bytes:       payload,
		ContentType: contentType,
		FileName:    generatedAssetFileName(taskID, index, contentType, kind),
		Scene:       store.FileStorageSceneImageResult,
		Source:      "ai-gateway",
	}
	if len(channels) == 0 {
		return nil, "", "", "", &clients.ClientError{Code: "storage_write_failed", Message: "no enabled object storage channel", StatusCode: http.StatusServiceUnavailable, Retryable: true}
	}
	upload, err := s.uploadFileWithFailover(ctx, uploadPayload, channels)
	if err == nil {
		s.observeResultStorage("uploaded")
	}
	return upload, contentType, kind, "upload_url_media", err
}

func uploadFileExtension(contentType string, fallbackExt string) string {
	normalized := normalizeGeneratedContentType(contentType)
	if generatedContentTypeIsMedia(normalized) {
		return fileExtensionForContentType(normalized, generatedAssetKindFromContentType("", normalized))
	}
	if normalized != "" && normalized != "application/octet-stream" {
		if extensions, err := mime.ExtensionsByType(normalized); err == nil && len(extensions) > 0 {
			if ext := sanitizeFileExtension(extensions[0]); ext != "" {
				return ext
			}
		}
	}
	if ext := sanitizeFileExtension(fallbackExt); ext != "" {
		return ext
	}
	if normalized == "application/json" {
		return ".json"
	}
	if strings.HasPrefix(normalized, "text/") {
		return ".txt"
	}
	return ".bin"
}

func sanitizeFileExtension(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	if value == "" {
		return ""
	}
	if !strings.HasPrefix(value, ".") {
		value = "." + value
	}
	if len(value) > 16 {
		return ""
	}
	for _, item := range value[1:] {
		if (item >= 'a' && item <= 'z') || (item >= '0' && item <= '9') {
			continue
		}
		return ""
	}
	return value
}

func (s *Service) readGeneratedURLAsset(ctx context.Context, asset *generatedURLAsset, acceptanceEmulatorBaseURL string) ([]byte, string, error) {
	fetchURL, err := s.generatedAssetFetchURL(asset.URL)
	if err != nil {
		return nil, "", err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL, nil)
	if err != nil {
		return nil, "", err
	}
	allowLocal := strings.HasPrefix(strings.TrimSpace(asset.URL), "/")
	allowAcceptanceOrigin := sameHTTPOrigin(asset.URL, acceptanceEmulatorBaseURL)
	resp, err := generatedAssetHTTPClient(allowLocal, allowAcceptanceOrigin).Do(req)
	if err != nil {
		return nil, "", &clients.ClientError{Code: "upload_source_fetch_failed", Message: err.Error(), Retryable: true}
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		message := strings.TrimSpace(string(body))
		if message == "" {
			message = "generated media source fetch failed"
		}
		return nil, "", &clients.ClientError{
			Code:       "upload_source_fetch_failed",
			Message:    message,
			StatusCode: resp.StatusCode,
			Retryable:  clients.HTTPRetryable(resp.StatusCode),
		}
	}
	payload, err := io.ReadAll(io.LimitReader(resp.Body, maxGeneratedAssetFetchBytes+1))
	if err != nil {
		return nil, "", &clients.ClientError{Code: "upload_source_read_failed", Message: err.Error(), StatusCode: resp.StatusCode, Retryable: clients.HTTPRetryable(resp.StatusCode)}
	}
	if len(payload) > maxGeneratedAssetFetchBytes {
		return nil, "", &clients.ClientError{Code: "upload_source_too_large", Message: "generated media source exceeds upload fetch limit", StatusCode: resp.StatusCode, Retryable: false}
	}
	contentType := firstNonEmptyString(resp.Header.Get("Content-Type"), asset.ContentType)
	return payload, strings.TrimSpace(strings.Split(contentType, ";")[0]), nil
}

func generatedAssetHTTPClient(allowLocal bool, allowAcceptanceOrigin bool) *http.Client {
	transport := http.DefaultTransport.(*http.Transport).Clone()
	transport.Proxy = nil
	transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
		host, port, err := net.SplitHostPort(address)
		if err != nil {
			return nil, err
		}
		addresses, err := net.DefaultResolver.LookupIPAddr(ctx, host)
		if err != nil || len(addresses) == 0 {
			return nil, errors.New("generated media DNS resolution failed")
		}
		for _, address := range addresses {
			if generatedAssetBlockedAddress(address.IP) &&
				!(allowLocal && address.IP.IsLoopback()) && !allowAcceptanceOrigin {
				return nil, errors.New("generated media resolved to a blocked network")
			}
		}
		dialer := &net.Dialer{Timeout: 10 * time.Second}
		var attempts []error
		for _, resolved := range addresses {
			connection, dialErr := dialer.DialContext(ctx, network, net.JoinHostPort(resolved.IP.String(), port))
			if dialErr == nil {
				return connection, nil
			}
			attempts = append(attempts, dialErr)
		}
		return nil, errors.Join(attempts...)
	}
	return &http.Client{
		Transport: transport,
		Timeout:   10 * time.Minute,
		CheckRedirect: func(*http.Request, []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
}

func sameHTTPOrigin(rawURL string, allowedBaseURL string) bool {
	raw, err := url.Parse(strings.TrimSpace(rawURL))
	if err != nil || raw.User != nil || (raw.Scheme != "http" && raw.Scheme != "https") || raw.Host == "" {
		return false
	}
	allowed, err := url.Parse(strings.TrimSpace(allowedBaseURL))
	if err != nil || allowed.User != nil || (allowed.Scheme != "http" && allowed.Scheme != "https") || allowed.Host == "" {
		return false
	}
	return strings.EqualFold(raw.Scheme, allowed.Scheme) && strings.EqualFold(raw.Host, allowed.Host)
}

func generatedAssetBlockedAddress(ip net.IP) bool {
	return ip == nil || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() || ip.IsMulticast()
}

func (s *Service) generatedAssetFetchURL(raw string) (string, error) {
	value := strings.TrimSpace(raw)
	if value == "" {
		return "", &clients.ClientError{Code: "upload_source_invalid_url", Message: "generated media source URL is empty", Retryable: false}
	}
	parsed, err := url.Parse(value)
	if err != nil {
		return "", &clients.ClientError{Code: "upload_source_invalid_url", Message: err.Error(), Retryable: false}
	}
	if parsed.IsAbs() {
		if parsed.Scheme == "http" || parsed.Scheme == "https" {
			return value, nil
		}
		return "", &clients.ClientError{Code: "upload_source_unsupported_url", Message: "unsupported generated media source URL scheme: " + parsed.Scheme, Retryable: false}
	}
	if strings.HasPrefix(value, "/") {
		baseURL := generatedAssetLocalBaseURL(s.cfg.HTTPAddr)
		if baseURL == "" {
			return "", &clients.ClientError{Code: "upload_source_invalid_url", Message: "generated media source uses a relative URL without a local HTTP address", Retryable: false}
		}
		return baseURL + value, nil
	}
	return "", &clients.ClientError{Code: "upload_source_invalid_url", Message: "generated media source URL must be absolute or root-relative", Retryable: false}
}

func generatedAssetLocalBaseURL(httpAddr string) string {
	addr := strings.TrimSpace(httpAddr)
	if addr == "" {
		return "http://127.0.0.1:8088"
	}
	if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") {
		return strings.TrimRight(addr, "/")
	}
	if strings.HasPrefix(addr, ":") {
		return "http://127.0.0.1" + addr
	}
	if strings.Contains(addr, "://") {
		return ""
	}
	return "http://" + strings.TrimRight(addr, "/")
}

func (s *Service) UploadFile(ctx context.Context, payload FileUploadPayload) (map[string]any, error) {
	if strings.TrimSpace(payload.Scene) == "" {
		payload.Scene = store.FileStorageSceneUpload
	}
	channels, err := s.activeFileStorageChannels(ctx, payload.Scene)
	if err != nil {
		return nil, &clients.ClientError{Code: "upload_config_failed", Message: err.Error(), Retryable: true}
	}
	if len(channels) == 0 {
		return nil, &clients.ClientError{Code: "storage_write_failed", Message: "no enabled object storage channel", StatusCode: http.StatusServiceUnavailable, Retryable: true}
	}
	return s.uploadFileWithFailover(ctx, payload, channels)
}

func (s *Service) activeFileStorageChannels(ctx context.Context, scene string) ([]store.FileStorageChannel, error) {
	channels := make([]store.FileStorageChannel, 0)
	if s.directOSS != nil && directOSSScene(scene) {
		channels = append(channels, s.directOSS.fileStorageChannel())
	}
	if s.store == nil {
		return channels, nil
	}
	storedChannels, err := s.store.ListEnabledFileStorageChannelsForScene(ctx, scene)
	if err != nil && !store.IsUndefinedDatabaseObject(err) {
		return nil, err
	}
	channels = append(channels, storedChannels...)
	sort.SliceStable(channels, func(i, j int) bool {
		if channels[i].Priority != channels[j].Priority {
			return channels[i].Priority < channels[j].Priority
		}
		return channels[i].ChannelKey < channels[j].ChannelKey
	})
	return channels, nil
}

func (s *Service) uploadFileWithFailover(ctx context.Context, payload FileUploadPayload, channels []store.FileStorageChannel) (map[string]any, error) {
	var lastErr error
	for index, channel := range channels {
		upload, err := s.uploadWithChannelRetries(ctx, payload, channel)
		if err == nil {
			if s.store != nil {
				_ = s.store.MarkFileStorageChannelSuccess(context.WithoutCancel(ctx), channel.ID)
			}
			return upload, nil
		}
		lastErr = err
		if s.store != nil {
			_ = s.store.MarkFileStorageChannelFailure(context.WithoutCancel(ctx), channel.ID, err.Error())
		}
		if !storageFailureAllowsFailover(channel, err) {
			return nil, err
		}
		if index+1 < len(channels) {
			s.observeObjectStorage("failover", channel.Provider, 0, 0)
		}
	}
	if lastErr != nil {
		s.observeObjectStorage("all_failed", "", 0, 0)
		return nil, &clients.ClientError{Code: "storage_write_failed", Message: "all configured object storage channels failed", StatusCode: http.StatusServiceUnavailable, Retryable: true}
	}
	return nil, &clients.ClientError{Code: "storage_write_failed", Message: "no enabled object storage channel", StatusCode: http.StatusServiceUnavailable, Retryable: true}
}

func storageFailureAllowsFailover(channel store.FileStorageChannel, err error) bool {
	code := strings.ToLower(strings.TrimSpace(clients.ErrorCode(err)))
	switch code {
	case "upload_source_too_large", "upload_decode_failed", "invalid_upstream_result", "invalid_multipart_file", "invalid_multipart_image", "invalid_multipart_audio":
		return false
	}
	var clientErr *clients.ClientError
	if !errors.As(err, &clientErr) {
		return true
	}
	switch clientErr.StatusCode {
	case http.StatusRequestEntityTooLarge, http.StatusUnsupportedMediaType, http.StatusUnprocessableEntity:
		return false
	case http.StatusBadRequest:
		// Object-storage 400 responses commonly indicate endpoint/signature
		// configuration and should move to the next channel. server-main 400 is
		// the existing upload API's request validation result.
		return !strings.EqualFold(strings.TrimSpace(channel.Provider), "server_main_openapi")
	default:
		return true
	}
}

func (s *Service) uploadWithChannelRetries(ctx context.Context, payload FileUploadPayload, channel store.FileStorageChannel) (map[string]any, error) {
	maxRetries, delays := uploadRetrySchedule(channel.RetryPolicy, channel.Provider)
	var lastErr error
	for attempt := 0; attempt <= maxRetries; attempt++ {
		startedAt := time.Now()
		upload, err := s.uploadOnce(ctx, payload, channel)
		if err == nil {
			s.observeObjectStorage("write_success", channel.Provider, len(payload.Bytes), time.Since(startedAt))
			return upload, nil
		}
		s.observeObjectStorage("write_failure", channel.Provider, 0, time.Since(startedAt))
		lastErr = err
		if attempt >= maxRetries || !clients.IsRetryable(err) {
			break
		}
		s.observeObjectStorage("retry", channel.Provider, 0, 0)
		delay := retryDelayForAttempt(attempt, delays)
		if err := sleepWithContext(ctx, delay); err != nil {
			return nil, err
		}
	}
	return nil, lastErr
}

func (s *Service) uploadOnce(ctx context.Context, payload FileUploadPayload, channel store.FileStorageChannel) (map[string]any, error) {
	switch strings.ToLower(strings.TrimSpace(channel.Provider)) {
	case "aliyun_oss", "s3":
		adapter, err := newObjectStorageAdapter(channel)
		if err != nil {
			return nil, err
		}
		return adapter.put(ctx, payload)
	case "server_main_openapi":
		return s.uploadServerMainOnce(ctx, payload, channel)
	default:
		return nil, &clients.ClientError{Code: "storage_config_invalid", Message: "unsupported file storage channel", Retryable: false}
	}
}

func (s *Service) uploadServerMainOnce(ctx context.Context, payload FileUploadPayload, channel store.FileStorageChannel) (map[string]any, error) {
	uploadURL := strings.TrimSpace(channel.UploadURL)
	if uploadURL == "" {
		uploadURL = defaultServerMainOpenAPIUploadURL
	}
	apiKey := strings.TrimSpace(channel.APIKey)
	if apiKey == "" {
		return nil, &clients.ClientError{Code: "missing_credentials", Message: "file storage channel API key is required", Retryable: false}
	}
	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	fileWriter, err := createUploadFormFile(writer, "file", firstNonEmptyString(payload.FileName, "upload.bin"), payload.ContentType)
	if err != nil {
		return nil, err
	}
	if _, err := fileWriter.Write(payload.Bytes); err != nil {
		return nil, err
	}
	_ = writer.WriteField("source", firstNonEmptyString(payload.Source, "ai-gateway"))
	_ = writer.WriteField("scene", firstNonEmptyString(payload.Scene, store.FileStorageSceneUpload))
	if err := writer.Close(); err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, &body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", writer.FormDataContentType())
	if payload.ContentType != "" {
		req.Header.Set("X-Upload-Content-Type", payload.ContentType)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, &clients.ClientError{Code: "upload_network", Message: err.Error(), Retryable: true}
	}
	defer resp.Body.Close()
	responseBody, readErr := io.ReadAll(resp.Body)
	if readErr != nil {
		return nil, &clients.ClientError{Code: "upload_read_failed", Message: readErr.Error(), StatusCode: resp.StatusCode, Retryable: clients.HTTPRetryable(resp.StatusCode)}
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		message := strings.TrimSpace(string(responseBody))
		if message == "" {
			message = "file upload failed"
		}
		return nil, &clients.ClientError{Code: "upload_failed", Message: message, StatusCode: resp.StatusCode, Retryable: clients.HTTPRetryable(resp.StatusCode)}
	}
	var decoded map[string]any
	if err := json.Unmarshal(responseBody, &decoded); err != nil {
		return nil, &clients.ClientError{Code: "upload_invalid_response", Message: err.Error(), Retryable: false}
	}
	return normalizeUploadResponse(decoded, channel), nil
}

func createUploadFormFile(writer *multipart.Writer, fieldName string, fileName string, contentType string) (io.Writer, error) {
	header := make(textproto.MIMEHeader)
	header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeMultipartValue(fieldName), escapeMultipartValue(fileName)))
	if strings.TrimSpace(contentType) != "" {
		header.Set("Content-Type", strings.TrimSpace(contentType))
	}
	return writer.CreatePart(header)
}

func escapeMultipartValue(value string) string {
	return strings.NewReplacer("\\", "\\\\", `"`, "\\\"").Replace(value)
}

func stripDataURLPrefix(value string) string {
	if index := strings.Index(value, ","); strings.HasPrefix(value, "data:") && index >= 0 {
		return value[index+1:]
	}
	return value
}

func generatedAssetDecisionForItem(taskKind string, item map[string]any, policy generatedAssetUploadPolicy) (generatedAssetDecision, error) {
	decision := generatedAssetDecision{}
	for _, key := range mediaURLCandidateKeys() {
		value := strings.TrimSpace(stringFromAny(item[key]))
		if value != "" && strings.Contains(value, "://") && !mediaURLString(value) {
			return decision, &clients.ClientError{Code: "invalid_upstream_result", Message: "generated media URL must use http or https", StatusCode: http.StatusBadGateway, Retryable: false}
		}
	}
	urlKey, mediaURL := mediaURLSourceFromItem(item)
	if mediaURL != "" {
		if !policy.UploadURLMedia {
			if !policy.PreserveInlineMedia {
				decision.StripKeys = inlineMediaKeys(item)
			}
			return decision, nil
		}
		contentType := mediaContentTypeFromItem(item)
		kind := mediaKindForAsset(taskKind, item, urlKey, contentType)
		decision.URL = &generatedURLAsset{
			URL:         mediaURL,
			ContentType: contentType,
			Kind:        kind,
			SourceKey:   urlKey,
		}
		decision.StripKeys = uniqueStringList(append(mediaURLKeys(item), inlineMediaKeys(item)...))
		return decision, nil
	}
	if !policy.UploadInlineMedia {
		return decision, nil
	}
	asset, keys, err := inlineAssetFromItem(taskKind, item)
	if err != nil {
		return decision, err
	}
	if asset == nil {
		return decision, nil
	}
	decision.Inline = asset
	decision.StripKeys = keys
	return decision, nil
}

func inlineAssetFromItem(taskKind string, item map[string]any) (*generatedInlineAsset, []string, error) {
	for _, key := range inlineMediaCandidateKeys() {
		value, ok := item[key]
		if !ok || value == nil {
			continue
		}
		if !inlineMediaKeyAllowedForItem(item, key, value) {
			continue
		}
		strictBase64 := inlineMediaKeyIsStrictBase64(key)
		payload, contentType, ok, err := inlineMediaPayload(value, strictBase64)
		if err != nil {
			return nil, nil, err
		}
		if !ok {
			continue
		}
		contentType = firstNonEmptyString(contentType, mediaContentTypeFromItem(item), defaultContentTypeForGeneratedAsset(mediaKindForAsset(taskKind, item, key, contentType)))
		kind := mediaKindForAsset(taskKind, item, key, contentType)
		return &generatedInlineAsset{
			Bytes:       payload,
			ContentType: contentType,
			Kind:        kind,
			SourceKey:   key,
		}, inlineMediaKeys(item), nil
	}
	return nil, nil, nil
}

func inlineMediaPayload(value any, strictBase64 bool) ([]byte, string, bool, error) {
	switch typed := value.(type) {
	case []byte:
		if len(typed) == 0 {
			return nil, "", false, nil
		}
		payload := make([]byte, len(typed))
		copy(payload, typed)
		return payload, "", true, nil
	case []any:
		payload, ok := bytesFromNumberArray(typed)
		return payload, "", ok, nil
	case map[string]any:
		if data, ok := typed["data"].([]any); ok {
			payload, ok := bytesFromNumberArray(data)
			return payload, firstNonEmptyString(stringFromAny(typed["mime_type"]), stringFromAny(typed["mimeType"])), ok, nil
		}
		if data, ok := typed["data"].([]byte); ok && len(data) > 0 {
			payload := make([]byte, len(data))
			copy(payload, data)
			return payload, firstNonEmptyString(stringFromAny(typed["mime_type"]), stringFromAny(typed["mimeType"])), true, nil
		}
		return nil, "", false, nil
	case string:
		return inlineMediaPayloadFromString(typed, strictBase64)
	default:
		return nil, "", false, nil
	}
}

func inlineMediaPayloadFromString(value string, strictBase64 bool) ([]byte, string, bool, error) {
	raw := strings.TrimSpace(value)
	if raw == "" || mediaURLString(raw) {
		return nil, "", false, nil
	}
	if strings.HasPrefix(strings.ToLower(raw), "data:") {
		contentType, encoded, ok, err := parseBase64DataURL(raw)
		if err != nil || !ok {
			return nil, "", false, err
		}
		payload, err := decodeBase64Payload(encoded)
		if err != nil {
			return nil, "", false, &clients.ClientError{Code: "invalid_upstream_result", Message: err.Error(), StatusCode: http.StatusBadGateway, Retryable: false}
		}
		return payload, contentType, true, nil
	}
	if !strictBase64 && len(raw) < 64 {
		return nil, "", false, nil
	}
	payload, err := decodeBase64Payload(raw)
	if err != nil {
		if strictBase64 {
			return nil, "", false, &clients.ClientError{Code: "invalid_upstream_result", Message: err.Error(), StatusCode: http.StatusBadGateway, Retryable: false}
		}
		return nil, "", false, nil
	}
	return payload, "", true, nil
}

func parseBase64DataURL(value string) (string, string, bool, error) {
	prefix, payload, ok := strings.Cut(value, ",")
	if !ok {
		return "", "", false, &clients.ClientError{Code: "invalid_upstream_result", Message: "invalid data URL media payload", StatusCode: http.StatusBadGateway, Retryable: false}
	}
	meta := strings.TrimPrefix(prefix, "data:")
	meta = strings.TrimPrefix(meta, "DATA:")
	parts := strings.Split(meta, ";")
	contentType := strings.TrimSpace(parts[0])
	isBase64 := false
	for _, part := range parts[1:] {
		if strings.EqualFold(strings.TrimSpace(part), "base64") {
			isBase64 = true
			break
		}
	}
	if !isBase64 {
		return "", "", false, &clients.ClientError{Code: "invalid_upstream_result", Message: "data URL media payload is not base64 encoded", StatusCode: http.StatusBadGateway, Retryable: false}
	}
	return contentType, payload, true, nil
}

func decodeBase64Payload(value string) ([]byte, error) {
	normalized := strings.Map(func(r rune) rune {
		switch r {
		case '\n', '\r', '\t', ' ':
			return -1
		default:
			return r
		}
	}, stripDataURLPrefix(value))
	encodings := []*base64.Encoding{
		base64.StdEncoding,
		base64.RawStdEncoding,
		base64.URLEncoding,
		base64.RawURLEncoding,
	}
	var lastErr error
	for _, encoding := range encodings {
		payload, err := encoding.DecodeString(normalized)
		if err == nil && len(payload) > 0 {
			return payload, nil
		}
		if err != nil {
			lastErr = err
		}
	}
	if lastErr == nil {
		lastErr = fmt.Errorf("empty base64 payload")
	}
	return nil, lastErr
}

func bytesFromNumberArray(values []any) ([]byte, bool) {
	if len(values) == 0 {
		return nil, false
	}
	payload := make([]byte, 0, len(values))
	for _, value := range values {
		next, ok := byteFromAny(value)
		if !ok {
			return nil, false
		}
		payload = append(payload, next)
	}
	return payload, true
}

func byteFromAny(value any) (byte, bool) {
	switch typed := value.(type) {
	case byte:
		return typed, true
	case int:
		if typed >= 0 && typed <= 255 {
			return byte(typed), true
		}
	case int64:
		if typed >= 0 && typed <= 255 {
			return byte(typed), true
		}
	case float64:
		asInt := int(typed)
		if typed == float64(asInt) && asInt >= 0 && asInt <= 255 {
			return byte(asInt), true
		}
	}
	return 0, false
}

func inlineMediaKeys(item map[string]any) []string {
	keys := []string{}
	for _, key := range inlineMediaCandidateKeys() {
		value, ok := item[key]
		if !ok || value == nil {
			continue
		}
		if !inlineMediaKeyAllowedForItem(item, key, value) {
			continue
		}
		strictBase64 := inlineMediaKeyIsStrictBase64(key)
		if strictBase64 && stringFromAny(value) != "" {
			keys = append(keys, key)
			continue
		}
		if _, _, ok, _ := inlineMediaPayload(value, strictBase64); ok {
			keys = append(keys, key)
		}
	}
	return uniqueStringList(keys)
}

func inlineMediaCandidateKeys() []string {
	return []string{
		"b64_json",
		"image_base64",
		"image_b64",
		"audio_base64",
		"audio_b64",
		"video_base64",
		"video_b64",
		"base64",
		"b64",
		"url",
		"image_url",
		"imageUrl",
		"audio_url",
		"audioUrl",
		"video_url",
		"videoUrl",
		"output_url",
		"outputUrl",
		"output_audio_url",
		"outputAudioUrl",
		"output_video_url",
		"outputVideoUrl",
		"image",
		"audio",
		"video",
		"image_buffer",
		"image_bytes",
		"audio_buffer",
		"audio_bytes",
		"video_buffer",
		"video_bytes",
		"buffer",
		"bytes",
		"data",
		"content",
	}
}

func inlineMediaKeyAllowedForItem(item map[string]any, key string, value any) bool {
	if !strings.EqualFold(strings.TrimSpace(key), "content") {
		return true
	}
	raw, ok := value.(string)
	if !ok {
		return generatedContentTypeIsMedia(mediaContentTypeFromItem(item)) || generatedResultItemIsMedia(item)
	}
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return false
	}
	if strings.HasPrefix(strings.ToLower(raw), "data:") {
		contentType, _, ok, err := parseBase64DataURL(raw)
		return err == nil && ok && generatedContentTypeIsMedia(contentType)
	}
	return generatedContentTypeIsMedia(mediaContentTypeFromItem(item)) || generatedResultItemIsMedia(item)
}

func generatedResultItemIsMedia(item map[string]any) bool {
	itemType := strings.ToLower(strings.TrimSpace(stringFromAny(item["type"])))
	return strings.Contains(itemType, "image") || strings.Contains(itemType, "video") || strings.Contains(itemType, "audio")
}

func inlineMediaKeyIsStrictBase64(key string) bool {
	lower := strings.ToLower(key)
	return lower == "b64_json" || lower == "base64" || lower == "b64" || strings.Contains(lower, "base64") || strings.Contains(lower, "_b64")
}

func mediaURLSourceFromItem(item map[string]any) (string, string) {
	for _, key := range mediaURLCandidateKeys() {
		value := stringFromAny(item[key])
		if value != "" && mediaURLString(value) {
			return key, value
		}
	}
	return "", ""
}

func mediaURLKeys(item map[string]any) []string {
	keys := []string{}
	for _, key := range mediaURLCandidateKeys() {
		value := stringFromAny(item[key])
		if value != "" && mediaURLString(value) {
			keys = append(keys, key)
		}
	}
	return uniqueStringList(keys)
}

func mediaURLCandidateKeys() []string {
	return []string{"url", "image_url", "imageUrl", "audio_url", "audioUrl", "video_url", "videoUrl", "output_url", "outputUrl", "output_audio_url", "outputAudioUrl", "output_video_url", "outputVideoUrl", "download_url", "downloadUrl", "file_url", "fileUrl"}
}

func mediaURLString(value string) bool {
	raw := strings.TrimSpace(value)
	if raw == "" {
		return false
	}
	lower := strings.ToLower(raw)
	if strings.HasPrefix(lower, "data:") {
		return false
	}
	if strings.HasPrefix(lower, "/") {
		return true
	}
	parsed, err := url.Parse(raw)
	return err == nil && parsed.User == nil && parsed.Host != "" && (parsed.Scheme == "http" || parsed.Scheme == "https")
}

func mediaContentTypeFromItem(item map[string]any) string {
	return firstNonEmptyString(
		stringFromAny(item["mime_type"]),
		stringFromAny(item["mimeType"]),
		stringFromAny(item["content_type"]),
		stringFromAny(item["contentType"]),
	)
}

func mediaKindForAsset(taskKind string, item map[string]any, sourceKey string, contentType string) string {
	contentType = strings.ToLower(strings.TrimSpace(contentType))
	if generatedContentTypeIsDocument(contentType) {
		return "file"
	}
	if strings.HasPrefix(contentType, "image/") {
		return "image"
	}
	if strings.HasPrefix(contentType, "video/") {
		return "video"
	}
	if strings.HasPrefix(contentType, "audio/") {
		return "audio"
	}
	itemType := strings.ToLower(strings.TrimSpace(stringFromAny(item["type"])))
	if itemType == "file" || strings.Contains(itemType, "document") {
		return "file"
	}
	if strings.Contains(itemType, "video") {
		return "video"
	}
	if strings.Contains(itemType, "image") {
		return "image"
	}
	key := strings.ToLower(sourceKey)
	if strings.Contains(key, "video") {
		return "video"
	}
	if strings.Contains(key, "image") {
		return "image"
	}
	kind := strings.ToLower(strings.TrimSpace(taskKind))
	if strings.Contains(kind, "video") {
		return "video"
	}
	if strings.Contains(kind, "audio") || strings.Contains(kind, "song") || strings.Contains(kind, "music") || strings.Contains(kind, "speech") {
		return "audio"
	}
	if strings.Contains(kind, "image") {
		return "image"
	}
	return "image"
}

func defaultContentTypeForGeneratedAsset(kind string) string {
	switch strings.ToLower(strings.TrimSpace(kind)) {
	case "video":
		return "video/mp4"
	case "audio":
		return "audio/mpeg"
	case "file":
		return "application/octet-stream"
	default:
		return "image/png"
	}
}

func resolvedGeneratedAssetContentType(declared string, kind string, payload []byte) string {
	declared = normalizeGeneratedContentType(declared)
	detected := detectGeneratedAssetContentType(payload)
	if generatedContentTypeIsMedia(detected) || generatedContentTypeIsDocument(detected) {
		return detected
	}
	if generatedContentTypeIsMedia(declared) || generatedContentTypeIsDocument(declared) {
		return declared
	}
	return defaultContentTypeForGeneratedAsset(kind)
}

func detectGeneratedAssetContentType(payload []byte) string {
	if len(payload) == 0 {
		return ""
	}
	return normalizeGeneratedContentType(http.DetectContentType(payload))
}

func normalizeGeneratedContentType(contentType string) string {
	return strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
}

func generatedContentTypeIsMedia(contentType string) bool {
	return strings.HasPrefix(contentType, "image/") ||
		strings.HasPrefix(contentType, "video/") ||
		strings.HasPrefix(contentType, "audio/")
}

func generatedContentTypeIsDocument(contentType string) bool {
	switch normalizeGeneratedContentType(contentType) {
	case "application/pdf", "application/postscript", "application/eps", "application/dxf", "image/vnd.dxf":
		return true
	default:
		return false
	}
}

func generatedAssetKindFromContentType(fallback string, contentType string) string {
	contentType = normalizeGeneratedContentType(contentType)
	if strings.HasPrefix(contentType, "image/") {
		return "image"
	}
	if strings.HasPrefix(contentType, "video/") {
		return "video"
	}
	if strings.HasPrefix(contentType, "audio/") {
		return "audio"
	}
	if generatedContentTypeIsDocument(contentType) {
		return "file"
	}
	fallback = strings.ToLower(strings.TrimSpace(fallback))
	if fallback != "" {
		return fallback
	}
	return "image"
}

func generatedAssetFileName(taskID string, index int, contentType string, kind string) string {
	token := sanitizeGeneratedAssetNamePart(taskID)
	if token == "" {
		token = fmt.Sprintf("%d", time.Now().UTC().UnixNano())
	}
	if len(token) > 32 {
		token = token[:32]
	}
	return fmt.Sprintf("gateway-result-%s-%02d-%s%s", token, index+1, randomHexSuffix(6), fileExtensionForContentType(contentType, kind))
}

func sanitizeGeneratedAssetNamePart(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	var builder strings.Builder
	for _, item := range value {
		if (item >= 'a' && item <= 'z') || (item >= '0' && item <= '9') || item == '-' || item == '_' {
			builder.WriteRune(item)
		}
	}
	return strings.Trim(builder.String(), "-_")
}

func randomHexSuffix(byteCount int) string {
	if byteCount <= 0 {
		byteCount = 6
	}
	payload := make([]byte, byteCount)
	if _, err := rand.Read(payload); err == nil {
		return hex.EncodeToString(payload)
	}
	return fmt.Sprintf("%d", time.Now().UTC().UnixNano())
}

func fileExtensionForContentType(contentType string, kind string) string {
	normalized := strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0]))
	switch normalized {
	case "application/pdf":
		return ".pdf"
	case "application/postscript", "application/eps":
		return ".eps"
	case "application/dxf", "image/vnd.dxf":
		return ".dxf"
	}
	switch normalized {
	case "image/jpeg", "image/jpg":
		return ".jpg"
	case "image/webp":
		return ".webp"
	case "image/gif":
		return ".gif"
	case "image/avif":
		return ".avif"
	case "image/bmp":
		return ".bmp"
	case "image/svg+xml":
		return ".svg"
	case "video/webm":
		return ".webm"
	case "video/quicktime":
		return ".mov"
	case "video/mp4":
		return ".mp4"
	case "audio/wav", "audio/x-wav":
		return ".wav"
	case "audio/ogg":
		return ".ogg"
	case "audio/mpeg", "audio/mp3":
		return ".mp3"
	case "image/png":
		return ".png"
	}
	if strings.EqualFold(kind, "video") {
		return ".mp4"
	}
	if strings.EqualFold(kind, "audio") {
		return ".mp3"
	}
	return ".png"
}

func uniqueStringList(values []string) []string {
	seen := map[string]bool{}
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" || seen[value] {
			continue
		}
		seen[value] = true
		out = append(out, value)
	}
	return out
}

func uploadRetrySchedule(policy map[string]any, providers ...string) (int, []time.Duration) {
	provider := ""
	if len(providers) > 0 {
		provider = providers[0]
	}
	if policy == nil {
		if strings.EqualFold(provider, "aliyun_oss") || strings.EqualFold(provider, "s3") {
			policy = map[string]any{"enabled": true, "maxRetries": 2, "backoffSeconds": []any{0.25, 1.0}}
		} else {
			policy = defaultUploadRetryPolicy()
		}
	}
	if enabled, ok := policy["enabled"].(bool); ok && !enabled {
		return 0, nil
	}
	maxRetries := intFromPolicy(policy, "maxRetries")
	if maxRetries <= 0 {
		maxRetries = 3
	}
	delays := uploadRetryDelays(policy["backoffSeconds"])
	if len(delays) == 0 {
		delays = []time.Duration{60 * time.Second, 120 * time.Second, 180 * time.Second}
	}
	return maxRetries, delays
}

func uploadRetryDelays(value any) []time.Duration {
	items, ok := value.([]any)
	if !ok {
		return nil
	}
	delays := make([]time.Duration, 0, len(items))
	for _, item := range items {
		seconds := floatFromAny(item)
		if seconds > 0 {
			delays = append(delays, time.Duration(seconds*float64(time.Second)))
		}
	}
	return delays
}

func retryDelayForAttempt(attempt int, delays []time.Duration) time.Duration {
	if attempt < len(delays) {
		return delays[attempt]
	}
	return delays[len(delays)-1]
}

func sleepWithContext(ctx context.Context, delay time.Duration) error {
	timer := time.NewTimer(delay)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

func defaultUploadRetryPolicy() map[string]any {
	return map[string]any{
		"enabled":        true,
		"maxRetries":     3,
		"backoffSeconds": []any{60, 120, 180},
		"strategy":       "exponential",
	}
}

func normalizeUploadResponse(decoded map[string]any, channel store.FileStorageChannel) map[string]any {
	if decoded == nil {
		decoded = map[string]any{}
	}
	if stringFromAny(decoded["url"]) == "" {
		if urlValue := uploadResponseURL(decoded); urlValue != "" {
			decoded["url"] = urlValue
		}
	}
	decoded["storageChannel"] = map[string]any{
		"id":         channel.ID,
		"channelKey": channel.ChannelKey,
		"name":       channel.Name,
		"provider":   channel.Provider,
	}
	return decoded
}

func uploadResponseURL(decoded map[string]any) string {
	for _, key := range []string{"url", "fileUrl", "file_url"} {
		if value := stringFromAny(decoded[key]); value != "" {
			return value
		}
	}
	for _, key := range []string{"data", "file", "result"} {
		if nested, ok := decoded[key].(map[string]any); ok {
			if value := uploadResponseURL(nested); value != "" {
				return value
			}
		}
		if items, ok := decoded[key].([]any); ok && len(items) > 0 {
			if nested, ok := items[0].(map[string]any); ok {
				if value := uploadResponseURL(nested); value != "" {
					return value
				}
			}
		}
	}
	return ""
}
