package backend

import (
	"database/sql"
	"encoding/json"
	"sort"
	"strings"
)

// mediaExtractWriteback is the normalized result used by media workers. The
// extractor interfaces remain string-based for compatibility with existing
// providers; structured attributes are therefore parsed opportunistically.
type mediaExtractWriteback struct {
	text       string
	tags       map[string]string
	structured bool
}

type mediaExtractStructuredResponse struct {
	Text        string                     `json:"text"`
	Transcript  string                     `json:"transcript"`
	ContentText string                     `json:"content_text"`
	Attributes  map[string]json.RawMessage `json:"attributes"`
}

// prepareMediaExtractWriteback preserves ordinary provider text while
// extracting the common attributes object used by image/audio/video prompts.
// Invalid attributes are dropped by the same validation used for images.
func prepareMediaExtractWriteback(raw string, maxBytes int) mediaExtractWriteback {
	candidate, ok := structuredImageExtractJSONCandidate(raw)
	if !ok {
		return mediaExtractWriteback{text: sanitizeExtractedText(raw, maxBytes), tags: map[string]string{}}
	}
	var response mediaExtractStructuredResponse
	var fields map[string]json.RawMessage
	if err := json.Unmarshal([]byte(candidate), &fields); err != nil {
		return mediaExtractWriteback{text: sanitizeExtractedText(raw, maxBytes), tags: map[string]string{}}
	}
	if _, hasText := fields["text"]; !hasText {
		if _, hasTranscript := fields["transcript"]; !hasTranscript {
			if _, hasContentText := fields["content_text"]; !hasContentText {
				if _, hasAttributes := fields["attributes"]; !hasAttributes {
					return mediaExtractWriteback{text: sanitizeExtractedText(raw, maxBytes), tags: map[string]string{}}
				}
			}
		}
	}
	if err := json.Unmarshal([]byte(candidate), &response); err != nil {
		return mediaExtractWriteback{text: sanitizeExtractedText(raw, maxBytes), tags: map[string]string{}}
	}
	tags := cleanImageExtractAttributes(response.Attributes)
	text := response.Text
	if strings.TrimSpace(text) == "" {
		text = response.Transcript
	}
	if strings.TrimSpace(text) == "" {
		text = response.ContentText
	}
	if strings.TrimSpace(text) == "" {
		text = buildMediaExtractAttributeText(tags)
	}
	return mediaExtractWriteback{text: sanitizeExtractedText(text, maxBytes), tags: tags, structured: response.Attributes != nil}
}

func buildMediaExtractAttributeText(tags map[string]string) string {
	if len(tags) == 0 {
		return ""
	}
	keys := make([]string, 0, len(tags))
	for key := range tags {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	pairs := make([]string, 0, len(keys))
	for _, key := range keys {
		pairs = append(pairs, key+"="+tags[key])
	}
	return "属性：" + strings.Join(pairs, "，")
}

func cloneMediaExtractTags(tags map[string]string) map[string]string {
	clone := make(map[string]string, len(tags))
	for key, value := range tags {
		clone[key] = value
	}
	return clone
}

// mergeMediaExtractTags applies incoming values over the current set and
// retains at most the provider contract's global attribute limit. Sorting the
// union makes the retained key set independent of map and checkpoint order.
func mergeMediaExtractTags(current, incoming map[string]string) map[string]string {
	merged, _ := mergeMediaExtractTagsWithLimit(current, incoming)
	return merged
}

func mergeMediaExtractTagsWithLimit(current, incoming map[string]string) (map[string]string, bool) {
	keys := make([]string, 0, len(current)+len(incoming))
	seen := make(map[string]struct{}, len(current)+len(incoming))
	for key := range current {
		seen[key] = struct{}{}
		keys = append(keys, key)
	}
	for key := range incoming {
		if _, ok := seen[key]; !ok {
			keys = append(keys, key)
		}
	}
	sort.Strings(keys)
	limited := len(keys) > maxImageExtractAttributes
	if limited {
		keys = keys[:maxImageExtractAttributes]
	}
	merged := make(map[string]string, len(keys))
	for _, key := range keys {
		if value, ok := incoming[key]; ok {
			merged[key] = value
		} else {
			merged[key] = current[key]
		}
	}
	return merged, limited
}

// mergeVideoExtractTags fixes modality precedence: audio attributes override
// visual attributes for the same key, regardless of checkpoint history.
func mergeVideoExtractTags(visual, audio map[string]string) map[string]string {
	return mergeMediaExtractTags(visual, audio)
}

func normalizeVideoExtractTagSets(visual, audio map[string]string) (map[string]string, map[string]string, bool) {
	merged, limited := mergeMediaExtractTagsWithLimit(visual, audio)
	normalizedVisual := make(map[string]string)
	normalizedAudio := make(map[string]string)
	for key, value := range merged {
		if _, ok := audio[key]; ok {
			normalizedAudio[key] = value
		} else {
			normalizedVisual[key] = value
		}
	}
	return normalizedVisual, normalizedAudio, limited
}

// updateMediaExtractTextTx preserves the existing revision gate while treating
// an already-empty content_text as a successful match. MySQL reports zero
// affected rows for that no-op update, so empty results need an explicit
// locked revision check before generated tags can be cleared safely.
func (b *Dat9Backend) updateMediaExtractTextTx(tx *sql.Tx, fileID string, expectedRevision int64, text string) (bool, error) {
	if text == "" {
		matched, err := b.store.MatchFileRevisionForUpdateTx(tx, fileID, expectedRevision)
		if err != nil || !matched {
			return matched, err
		}
		_, err = b.store.UpdateFileSearchTextTx(tx, fileID, expectedRevision, text)
		return true, err
	}
	updated, err := b.store.UpdateFileSearchTextTx(tx, fileID, expectedRevision, text)
	return updated, err
}
