package httpapi

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"mime"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"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/config"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/runner"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

const requestAssetFilePrefix = "gateway-request-asset-"

type preparedTaskRequest struct {
	Body            map[string]any
	ConversationID  string
	MessageRefs     []store.TaskMessageRefInput
	NewMessageCount int
}

type decodedRequestAsset struct {
	Bytes       []byte
	ContentType string
}

type requestAssetLock struct {
	mu   sync.Mutex
	refs int
}

func (s *Server) prepareTaskRequest(ctx context.Context, r *http.Request, user *auth.User, body map[string]any) (preparedTaskRequest, error) {
	preparedBody, err := s.prepareRequestAssetRefs(ctx, body)
	if err != nil {
		return preparedTaskRequest{}, err
	}
	result := preparedTaskRequest{Body: preparedBody}
	conversationKey := requestConversationKey(r, preparedBody)
	if conversationKey == "" {
		return result, nil
	}
	messages, ok := preparedBody["messages"].([]any)
	if !ok || len(messages) == 0 {
		return result, nil
	}
	conversationID, err := s.store.EnsureConversation(ctx, user, conversationKey, map[string]any{
		"source":          "ai-gateway",
		"conversationKey": conversationKey,
	})
	if err != nil {
		return preparedTaskRequest{}, err
	}
	inputs := make([]store.ConversationMessageInput, 0, len(messages))
	messageHashes := make([]any, 0, len(messages))
	for _, rawMessage := range messages {
		message, _ := rawMessage.(map[string]any)
		if message == nil {
			message = map[string]any{"content": rawMessage}
		}
		hash, assetHashes := canonicalConversationMessageHash(message)
		messageHashes = append(messageHashes, hash)
		inputs = append(inputs, store.ConversationMessageInput{
			Hash:         hash,
			Role:         stringFromRequestAny(message["role"]),
			Snapshot:     message,
			AssetSHA256s: assetHashes,
		})
	}
	refs, newCount, err := s.store.UpsertConversationMessages(ctx, conversationID, inputs)
	if err != nil {
		return preparedTaskRequest{}, err
	}
	preparedBody["conversationId"] = conversationKey
	preparedBody["conversationRecordId"] = conversationID
	preparedBody["messageRefs"] = messageRefsForRequest(refs)
	preparedBody["messageHashes"] = messageHashes
	preparedBody["newMessageCount"] = newCount
	delete(preparedBody, "messages")
	result.ConversationID = conversationID
	result.MessageRefs = refs
	result.NewMessageCount = newCount
	return result, nil
}

func (s *Server) prepareRequestAssetRefs(ctx context.Context, body map[string]any) (map[string]any, error) {
	value, err := s.prepareRequestAssetValue(ctx, body, nil, nil)
	if err != nil {
		return nil, err
	}
	next, _ := value.(map[string]any)
	if next == nil {
		return map[string]any{}, nil
	}
	return next, nil
}

func (s *Server) prepareRequestAssetValue(ctx context.Context, value any, path []string, siblings map[string]any) (any, error) {
	if decoded, ok, err := requestAssetFromBinaryValue(requestAssetPathKey(path), path, value, siblings); err != nil {
		return nil, err
	} else if ok {
		if err := s.acquireMediaRequestSlot(ctx); err != nil {
			return nil, err
		}
		defer s.releaseMediaRequestSlot()
		ref, err := s.ensureRequestAsset(ctx, decoded)
		if err != nil {
			return nil, err
		}
		return requestAssetWrapper(ref), nil
	}
	switch typed := value.(type) {
	case map[string]any:
		if typed["assetRef"] != nil {
			return typed, nil
		}
		next := make(map[string]any, len(typed))
		keys := make([]string, 0, len(typed))
		for key := range typed {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		for _, key := range keys {
			item := typed[key]
			if ref, ok, err := s.prepareRequestAssetField(ctx, key, path, item, typed); err != nil {
				return nil, err
			} else if ok {
				next[key] = requestAssetWrapper(ref)
				continue
			}
			prepared, err := s.prepareRequestAssetValue(ctx, item, append(path, key), typed)
			if err != nil {
				return nil, err
			}
			next[key] = prepared
		}
		return next, nil
	case []any:
		next := make([]any, 0, len(typed))
		for index, item := range typed {
			prepared, err := s.prepareRequestAssetValue(ctx, item, append(path, fmt.Sprintf("[%d]", index)), siblings)
			if err != nil {
				return nil, err
			}
			next = append(next, prepared)
		}
		return next, nil
	default:
		return value, nil
	}
}

func requestAssetFromBinaryValue(key string, path []string, value any, siblings map[string]any) (decodedRequestAsset, bool, error) {
	var payload []byte
	contentType := ""
	switch typed := value.(type) {
	case []byte:
		payload = append([]byte(nil), typed...)
	case map[string]any:
		if !strings.EqualFold(strings.TrimSpace(stringFromRequestAny(typed["type"])), "buffer") {
			return decodedRequestAsset{}, false, nil
		}
		contentType = firstNonEmptyRequestString(typed, "contentType", "mimeType", "mime_type")
		switch data := typed["data"].(type) {
		case []byte:
			payload = append([]byte(nil), data...)
		case []any:
			var ok bool
			payload, ok = requestBytesFromNumberArray(data)
			if !ok {
				return decodedRequestAsset{}, false, requestAssetDecodeError(fmt.Errorf("Buffer data must contain byte values"))
			}
		default:
			return decodedRequestAsset{}, false, requestAssetDecodeError(fmt.Errorf("Buffer data is required"))
		}
	case []any:
		if !strictRequestBinaryArrayField(key, path) {
			return decodedRequestAsset{}, false, nil
		}
		var ok bool
		payload, ok = requestBytesFromNumberArray(typed)
		if !ok {
			return decodedRequestAsset{}, false, nil
		}
	default:
		return decodedRequestAsset{}, false, nil
	}
	if len(payload) == 0 {
		return decodedRequestAsset{}, false, requestAssetDecodeError(fmt.Errorf("binary media payload is empty"))
	}
	return decodedRequestAsset{
		Bytes:       payload,
		ContentType: requestAssetContentType(contentType, payload, key, path, siblings),
	}, true, nil
}

func requestAssetPathKey(path []string) string {
	for index := len(path) - 1; index >= 0; index-- {
		value := strings.TrimSpace(path[index])
		if value != "" && !strings.HasPrefix(value, "[") {
			return value
		}
	}
	return ""
}

func strictRequestBinaryArrayField(key string, path []string) bool {
	value := strings.ToLower(strings.TrimSpace(key))
	if value == "bytes" || value == "buffer" || strings.Contains(value, "binary") || strings.Contains(value, "buffer") {
		return true
	}
	return requestMediaKind(key, path, nil) != "" && (value == "data" || value == "content")
}

func requestBytesFromNumberArray(values []any) ([]byte, bool) {
	if len(values) == 0 {
		return nil, false
	}
	payload := make([]byte, len(values))
	for index, value := range values {
		number, ok := value.(float64)
		if !ok || number < 0 || number > 255 || number != float64(byte(number)) {
			return nil, false
		}
		payload[index] = byte(number)
	}
	return payload, true
}

func (s *Server) prepareRequestAssetField(ctx context.Context, key string, path []string, value any, siblings map[string]any) (map[string]any, bool, error) {
	text, ok := value.(string)
	if !ok {
		return nil, false, nil
	}
	raw := strings.TrimSpace(text)
	if raw == "" || mediaURLString(raw) {
		return nil, false, nil
	}
	if !strings.HasPrefix(strings.ToLower(raw), "data:") &&
		!strictRequestBase64Field(key, path) &&
		!likelyRequestBase64MediaField(key, path, raw) {
		return nil, false, nil
	}
	if err := s.acquireMediaRequestSlot(ctx); err != nil {
		return nil, false, err
	}
	defer s.releaseMediaRequestSlot()
	decoded, ok, err := requestAssetFromValue(key, path, value, siblings)
	if err != nil || !ok {
		return nil, ok, err
	}
	ref, err := s.ensureRequestAsset(ctx, decoded)
	return ref, true, err
}

func (s *Server) acquireMediaRequestSlot(ctx context.Context) error {
	if s.mediaRequestSlots == nil {
		return nil
	}
	select {
	case s.mediaRequestSlots <- struct{}{}:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (s *Server) releaseMediaRequestSlot() {
	if s.mediaRequestSlots != nil {
		<-s.mediaRequestSlots
	}
}

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

type mediaRequestBodyLeaseContextKey struct{}

type mediaRequestBodyLease struct {
	release func()
}

func (l *mediaRequestBodyLease) Release() {
	if l != nil && l.release != nil {
		l.release()
	}
}

func (s *Server) withMediaRequestBodySlot(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		release, err := s.acquireMediaRequestBodySlot(r.Context())
		if err != nil {
			return
		}
		lease := &mediaRequestBodyLease{release: release}
		defer lease.Release()
		ctx := context.WithValue(r.Context(), mediaRequestBodyLeaseContextKey{}, lease)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func (s *Server) requestMediaBodyRelease(ctx context.Context) (func(), error) {
	if lease, ok := ctx.Value(mediaRequestBodyLeaseContextKey{}).(*mediaRequestBodyLease); ok && lease != nil {
		return lease.Release, nil
	}
	return s.acquireMediaRequestBodySlot(ctx)
}

func requestAssetFromValue(key string, path []string, value any, siblings map[string]any) (decodedRequestAsset, bool, error) {
	text, ok := value.(string)
	if !ok {
		return decodedRequestAsset{}, false, nil
	}
	raw := strings.TrimSpace(text)
	if raw == "" || mediaURLString(raw) {
		return decodedRequestAsset{}, false, nil
	}
	if strings.HasPrefix(strings.ToLower(raw), "data:") {
		contentType, encoded, ok, err := parseRequestDataURL(raw)
		if err != nil || !ok {
			return decodedRequestAsset{}, false, err
		}
		payload, err := decodeRequestBase64(encoded)
		if err != nil {
			return decodedRequestAsset{}, false, requestAssetDecodeError(err)
		}
		return decodedRequestAsset{
			Bytes:       payload,
			ContentType: requestAssetContentType(contentType, payload, key, path, siblings),
		}, true, nil
	}
	strict := strictRequestBase64Field(key, path)
	if !strict && !likelyRequestBase64MediaField(key, path, raw) {
		return decodedRequestAsset{}, false, nil
	}
	payload, err := decodeRequestBase64(raw)
	if err != nil {
		if strict {
			return decodedRequestAsset{}, false, requestAssetDecodeError(err)
		}
		return decodedRequestAsset{}, false, nil
	}
	contentType := requestAssetContentType("", payload, key, path, siblings)
	if !strict && !requestContentTypeIsMedia(contentType) {
		return decodedRequestAsset{}, false, nil
	}
	return decodedRequestAsset{Bytes: payload, ContentType: contentType}, true, nil
}

func (s *Server) ensureRequestAsset(ctx context.Context, decoded decodedRequestAsset) (map[string]any, error) {
	sum := sha256.Sum256(decoded.Bytes)
	sha := hex.EncodeToString(sum[:])
	contentType := strings.TrimSpace(decoded.ContentType)
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	release := s.acquireRequestAssetLock(sha + "\x00" + contentType + "\x00" + store.FileStorageSceneRequestAsset)
	defer release()
	now := time.Now()
	if existing, ok, err := s.store.FindRequestAsset(ctx, sha, contentType); err != nil && !store.IsUndefinedDatabaseObject(err) {
		return nil, err
	} else if ok && requestAssetStillUsable(existing, now) {
		ref := requestAssetRef(existing)
		if err := s.store.IncrementRequestAssetRefCount(ctx, sha, contentType); err != nil && !store.IsUndefinedDatabaseObject(err) {
			return nil, err
		}
		return ref, nil
	}

	upload, err := s.runner.UploadFile(ctx, runner.FileUploadPayload{
		Bytes:       decoded.Bytes,
		ContentType: contentType,
		FileName:    requestAssetFileName(sha, contentType),
		Scene:       store.FileStorageSceneRequestAsset,
		Source:      "ai-gateway-request",
	})
	if err != nil {
		return nil, err
	}
	storageProvider := requestAssetStorageProvider(upload)
	storageChannelID, storageChannelKey := requestAssetStorageChannel(upload)
	objectKey := stringFromRequestAny(upload["objectKey"])
	accessScope := firstNonEmpty(stringFromRequestAny(upload["accessScope"]), "private")
	url := stringFromRequestAny(upload["url"])
	if url == "" {
		return nil, &clients.ClientError{Code: "request_asset_upload_failed", Message: "file storage response did not include url", Retryable: false}
	}
	var expiresAt *time.Time
	localPath := ""
	if storageProvider == "local_static" {
		expiry := now.Add(time.Duration(s.localTempAssetTTLHours()) * time.Hour)
		expiresAt = &expiry
		localPath = requestAssetLocalPath(s.cfg.LocalUploadedStorageDir, stringFromRequestAny(upload["fileName"]))
	} else {
		expiresAt = requestAssetUploadExpiresAt(upload)
	}
	asset, err := s.store.UpsertRequestAsset(ctx, store.RequestAssetInput{
		SHA256:            sha,
		ContentType:       contentType,
		ByteSize:          int64(len(decoded.Bytes)),
		URL:               url,
		StorageProvider:   storageProvider,
		StorageChannelID:  storageChannelID,
		StorageChannelKey: storageChannelKey,
		ObjectKey:         objectKey,
		AccessScope:       accessScope,
		LocalPath:         localPath,
		ExpiresAt:         expiresAt,
	})
	if err != nil {
		if store.IsUndefinedDatabaseObject(err) {
			return map[string]any{
				"sha256":            sha,
				"url":               url,
				"contentType":       contentType,
				"size":              len(decoded.Bytes),
				"storageProvider":   storageProvider,
				"storageChannelId":  storageChannelID,
				"storageChannelKey": storageChannelKey,
				"objectKey":         objectKey,
				"accessScope":       accessScope,
				"expiresAt":         timePtrToRFC3339(expiresAt),
			}, nil
		}
		return nil, err
	}
	return requestAssetRef(asset), nil
}

func (s *Server) acquireRequestAssetLock(key string) func() {
	s.requestAssetLocksMu.Lock()
	if s.requestAssetLocks == nil {
		s.requestAssetLocks = map[string]*requestAssetLock{}
	}
	entry := s.requestAssetLocks[key]
	if entry == nil {
		entry = &requestAssetLock{}
		s.requestAssetLocks[key] = entry
	}
	entry.refs++
	s.requestAssetLocksMu.Unlock()

	entry.mu.Lock()
	return func() {
		entry.mu.Unlock()
		s.requestAssetLocksMu.Lock()
		entry.refs--
		if entry.refs == 0 {
			delete(s.requestAssetLocks, key)
		}
		s.requestAssetLocksMu.Unlock()
	}
}

func requestConversationKey(r *http.Request, body map[string]any) string {
	if r != nil {
		if value := strings.TrimSpace(r.Header.Get("X-EasyAI-Conversation-ID")); value != "" {
			return value
		}
	}
	if value := firstNonEmptyRequestString(body, "conversation_id", "conversationId"); value != "" {
		return value
	}
	if metadata, ok := body["metadata"].(map[string]any); ok {
		return firstNonEmptyRequestString(metadata, "conversation_id", "conversationId")
	}
	return ""
}

func messageRefsForRequest(refs []store.TaskMessageRefInput) []any {
	out := make([]any, 0, len(refs))
	for _, ref := range refs {
		out = append(out, map[string]any{"messageId": ref.MessageID, "position": ref.Position})
	}
	return out
}

func canonicalConversationMessageHash(message map[string]any) (string, []string) {
	parts := map[string]any{
		"role": stringFromRequestAny(message["role"]),
	}
	texts := []string{}
	assetHashes := []string{}
	collectConversationMessageParts(message, &texts, &assetHashes)
	parts["text"] = strings.Join(texts, "\n")
	parts["assets"] = assetHashes
	if parts["text"] == "" && len(assetHashes) == 0 {
		encoded, _ := json.Marshal(message)
		parts["fallback"] = string(encoded)
	}
	encoded, _ := json.Marshal(parts)
	sum := sha256.Sum256(encoded)
	return hex.EncodeToString(sum[:]), uniqueRequestStrings(assetHashes)
}

func collectConversationMessageParts(value any, texts *[]string, assetHashes *[]string) {
	switch typed := value.(type) {
	case map[string]any:
		if ref, ok := typed["assetRef"].(map[string]any); ok {
			if sha := stringFromRequestAny(ref["sha256"]); sha != "" {
				*assetHashes = append(*assetHashes, sha)
			}
		}
		for key, item := range typed {
			switch key {
			case "content", "text":
				if text := stringFromRequestAny(item); text != "" && !strings.HasPrefix(strings.ToLower(text), "data:") {
					*texts = append(*texts, text)
					continue
				}
			}
			switch item.(type) {
			case map[string]any, []any:
				collectConversationMessageParts(item, texts, assetHashes)
			}
		}
	case []any:
		for _, item := range typed {
			collectConversationMessageParts(item, texts, assetHashes)
		}
	case string:
		text := strings.TrimSpace(typed)
		if text != "" && !mediaURLString(text) && !strings.HasPrefix(strings.ToLower(text), "data:") {
			*texts = append(*texts, text)
		}
	}
}

func requestAssetWrapper(ref map[string]any) map[string]any {
	return map[string]any{
		"assetRef": ref,
		"url":      ref["url"],
	}
}

func requestAssetRef(asset store.RequestAsset) map[string]any {
	return map[string]any{
		"sha256":            asset.SHA256,
		"url":               asset.URL,
		"contentType":       asset.ContentType,
		"size":              asset.ByteSize,
		"storageProvider":   asset.StorageProvider,
		"storageChannelId":  asset.StorageChannelID,
		"storageChannelKey": asset.StorageChannelKey,
		"objectKey":         asset.ObjectKey,
		"accessScope":       asset.AccessScope,
		"expiresAt":         timePtrToRFC3339(asset.ExpiresAt),
	}
}

func requestAssetStillUsable(asset store.RequestAsset, now time.Time) bool {
	if asset.ExpiredAt != nil {
		return false
	}
	if asset.ExpiresAt != nil && !asset.ExpiresAt.After(now.Add(30*time.Second)) {
		return false
	}
	if strings.EqualFold(strings.TrimSpace(asset.StorageProvider), "local_static") {
		localPath := strings.TrimSpace(asset.LocalPath)
		info, err := os.Stat(localPath)
		if localPath == "" || err != nil || !info.Mode().IsRegular() {
			return false
		}
		if asset.ByteSize > 0 && info.Size() != asset.ByteSize {
			return false
		}
	}
	return strings.TrimSpace(asset.URL) != ""
}

func requestAssetUploadExpiresAt(upload map[string]any) *time.Time {
	var earliest *time.Time
	for _, key := range []string{"urlExpiresAt", "objectExpiresAt"} {
		value := strings.TrimSpace(stringFromRequestAny(upload[key]))
		if value == "" {
			continue
		}
		parsed, err := time.Parse(time.RFC3339, value)
		if err != nil {
			continue
		}
		if earliest == nil || parsed.Before(*earliest) {
			candidate := parsed
			earliest = &candidate
		}
	}
	return earliest
}

func requestAssetStorageProvider(upload map[string]any) string {
	if channel, ok := upload["storageChannel"].(map[string]any); ok {
		if provider := stringFromRequestAny(channel["provider"]); provider != "" {
			return provider
		}
	}
	return "unknown"
}

func requestAssetStorageChannel(upload map[string]any) (string, string) {
	channel, _ := upload["storageChannel"].(map[string]any)
	return stringFromRequestAny(channel["id"]), stringFromRequestAny(channel["channelKey"])
}

func requestAssetLocalPath(storageDir string, fileName string) string {
	if strings.TrimSpace(storageDir) == "" {
		storageDir = config.DefaultLocalUploadedStorageDir
	}
	if strings.TrimSpace(fileName) == "" {
		return ""
	}
	return filepath.Join(storageDir, filepath.Base(fileName))
}

func requestAssetFileName(sha string, contentType string) string {
	prefix := sha
	if len(prefix) > 16 {
		prefix = prefix[:16]
	}
	return requestAssetFilePrefix + prefix + requestAssetExtension(contentType)
}

func requestAssetExtension(contentType string) string {
	switch strings.ToLower(strings.TrimSpace(contentType)) {
	case "image/png":
		return ".png"
	case "image/jpeg", "image/jpg":
		return ".jpg"
	case "image/webp":
		return ".webp"
	case "image/gif":
		return ".gif"
	case "audio/mpeg":
		return ".mp3"
	case "audio/wav", "audio/x-wav":
		return ".wav"
	case "video/mp4":
		return ".mp4"
	}
	if extensions, err := mime.ExtensionsByType(contentType); err == nil && len(extensions) > 0 {
		return extensions[0]
	}
	return ".bin"
}

func requestAssetContentType(explicit string, payload []byte, key string, path []string, siblings map[string]any) string {
	if contentType := firstNonEmptyRequestString(siblings, "content_type", "contentType", "mime_type", "mimeType"); contentType != "" {
		return contentType
	}
	if explicit = strings.TrimSpace(explicit); explicit != "" {
		return explicit
	}
	detected := ""
	if len(payload) > 0 {
		detected = http.DetectContentType(payload)
	}
	if requestContentTypeIsMedia(detected) {
		return detected
	}
	switch requestMediaKind(key, path, siblings) {
	case "audio":
		return "audio/mpeg"
	case "video":
		return "video/mp4"
	default:
		return "image/png"
	}
}

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

func decodeRequestBase64(value string) ([]byte, error) {
	normalized := strings.Map(func(r rune) rune {
		switch r {
		case '\n', '\r', '\t', ' ':
			return -1
		default:
			return r
		}
	}, strings.TrimSpace(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 requestAssetDecodeError(err error) error {
	return &clients.ClientError{Code: "request_asset_decode_failed", Message: err.Error(), Retryable: false}
}

func strictRequestBase64Field(key string, path []string) bool {
	lower := strings.ToLower(strings.TrimSpace(key))
	parent := ""
	if len(path) > 0 {
		parent = strings.ToLower(strings.Trim(path[len(path)-1], "[]"))
	}
	return lower == "b64_json" ||
		lower == "base64" ||
		lower == "b64" ||
		strings.Contains(lower, "base64") ||
		strings.Contains(lower, "_b64") ||
		(parent == "input_audio" && lower == "data") ||
		((parent == "inlinedata" || parent == "inline_data") && lower == "data")
}

func likelyRequestBase64MediaField(key string, path []string, value string) bool {
	if len(value) < 64 {
		return false
	}
	return requestMediaKind(key, path, nil) != ""
}

func requestMediaKind(key string, path []string, siblings map[string]any) string {
	candidates := append([]string{key}, path...)
	if siblings != nil {
		candidates = append(candidates, stringFromRequestAny(siblings["type"]), stringFromRequestAny(siblings["role"]))
	}
	for _, item := range candidates {
		lower := strings.ToLower(strings.TrimSpace(item))
		switch {
		case strings.Contains(lower, "audio"):
			return "audio"
		case strings.Contains(lower, "video"):
			return "video"
		case strings.Contains(lower, "image") || lower == "mask" || lower == "b64_json":
			return "image"
		}
	}
	return ""
}

func requestContentTypeIsMedia(contentType string) bool {
	lower := strings.ToLower(strings.TrimSpace(contentType))
	return strings.HasPrefix(lower, "image/") ||
		strings.HasPrefix(lower, "video/") ||
		strings.HasPrefix(lower, "audio/")
}

func mediaURLString(value string) bool {
	raw := strings.TrimSpace(value)
	if raw == "" {
		return false
	}
	lower := strings.ToLower(raw)
	if strings.HasPrefix(lower, "data:") {
		return false
	}
	return strings.HasPrefix(lower, "http://") ||
		strings.HasPrefix(lower, "https://") ||
		strings.HasPrefix(lower, "/") ||
		strings.Contains(lower, "://")
}

func firstNonEmptyRequestString(values map[string]any, keys ...string) string {
	for _, key := range keys {
		if values == nil {
			continue
		}
		if value := stringFromRequestAny(values[key]); value != "" {
			return value
		}
	}
	return ""
}

func stringFromRequestAny(value any) string {
	text, _ := value.(string)
	return strings.TrimSpace(text)
}

func uniqueRequestStrings(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 timePtrToRFC3339(value *time.Time) any {
	if value == nil {
		return nil
	}
	return value.UTC().Format(time.RFC3339)
}

func (s *Server) localTempAssetTTLHours() int {
	if s.cfg.LocalTempAssetTTLHours <= 0 {
		return 24
	}
	return s.cfg.LocalTempAssetTTLHours
}
