package mapping

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	"qctextbuilder/internal/domain"
	"qctextbuilder/internal/qcclient"
)

type SuggestionGenerator interface {
	Generate(ctx context.Context, req SuggestionRequest) (SuggestionResult, error)
}

type RuleBasedSuggestionGenerator struct{}

func NewRuleBasedSuggestionGenerator() *RuleBasedSuggestionGenerator {
	return &RuleBasedSuggestionGenerator{}
}

func (g *RuleBasedSuggestionGenerator) Generate(_ context.Context, req SuggestionRequest) (SuggestionResult, error) {
	result := SuggestFieldValuesRuleBased(req)
	mappingLogger().Info("rule-based suggestion used",
		"component", "autofill",
		"step", "rule_based_fallback",
		"status", "success",
		"draft_id", strings.TrimSpace(req.DraftID),
		"template_id", req.TemplateID,
		"suggestion_count", len(result.Suggestions),
	)
	return result, nil
}

type LLMSuggestionGenerator struct {
	qc     qcclient.Client
	source string
}

func NewLLMSuggestionGenerator(qc qcclient.Client) *LLMSuggestionGenerator {
	return &LLMSuggestionGenerator{
		qc:     qc,
		source: domain.DraftSuggestionSourceLLM,
	}
}

func NewQCLLMSuggestionGenerator(qc qcclient.Client) *LLMSuggestionGenerator {
	return &LLMSuggestionGenerator{
		qc:     qc,
		source: domain.DraftSuggestionSourceQCLLM,
	}
}

func (g *LLMSuggestionGenerator) Generate(ctx context.Context, req SuggestionRequest) (SuggestionResult, error) {
	started := time.Now()
	source := ""
	if g != nil {
		source = strings.TrimSpace(g.source)
	}
	step := "qc_fallback_request"
	if source == domain.DraftSuggestionSourceLLM {
		step = "llm_request"
	}
	mappingLogger().InfoContext(ctx, "llm suggestion request",
		"component", "autofill",
		"step", step,
		"status", "start",
		"source", source,
		"draft_id", strings.TrimSpace(req.DraftID),
		"template_id", req.TemplateID,
	)
	if g == nil || g.qc == nil {
		mappingLogger().WarnContext(ctx, "llm suggestion request",
			"component", "autofill",
			"step", step,
			"status", "failed",
			"source", source,
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"error", "llm generator is not configured",
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, fmt.Errorf("llm generator is not configured")
	}
	if req.TemplateID <= 0 {
		mappingLogger().WarnContext(ctx, "llm suggestion request",
			"component", "autofill",
			"step", step,
			"status", "failed",
			"source", source,
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"error", "template id is required for llm suggestions",
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, fmt.Errorf("template id is required for llm suggestions")
	}

	fieldByPath := make(map[string]domain.TemplateField, len(req.Fields))
	for _, field := range req.Fields {
		if !field.IsEnabled || !strings.EqualFold(strings.TrimSpace(field.FieldKind), "text") {
			continue
		}
		fieldByPath[field.Path] = field
	}

	targets := collectSuggestionTargets(req.Fields, req.Existing, req.IncludeFilled)
	customTemplateData := map[string]any{
		"_autofill": map[string]any{
			"masterPrompt": strings.TrimSpace(req.MasterPrompt),
			"promptBlocks": enabledPromptBlocks(req.PromptBlocks),
			"draftContext": llmDraftContextMap(req.DraftContext),
			"semanticSlots": func() map[string]string {
				out := make(map[string]string, len(targets))
				for _, target := range targets {
					out[target.FieldPath] = target.Slot
				}
				return out
			}(),
		},
	}
	resp, _, err := g.qc.GenerateContent(ctx, qcclient.GenerateContentRequest{
		TemplateID:         req.TemplateID,
		GlobalData:         req.GlobalData,
		Empty:              true,
		ToneOfVoice:        contentTone(req.DraftContext),
		TargetAudience:     targetAudience(req),
		CustomTemplateData: customTemplateData,
	})
	if err != nil {
		mappingLogger().WarnContext(ctx, "llm suggestion request",
			"component", "autofill",
			"step", step,
			"status", "failed",
			"source", source,
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"error", shortErr(err),
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, err
	}

	out := SuggestionResult{
		Suggestions: make([]Suggestion, 0, len(targets)),
		ByFieldPath: map[string]Suggestion{},
	}
	for _, target := range targets {
		field, ok := fieldByPath[target.FieldPath]
		if !ok {
			continue
		}
		sectionData := resp[field.Section]
		if sectionData == nil {
			continue
		}
		raw, ok := sectionData[field.KeyName]
		if !ok {
			continue
		}
		value := normalizeLLMValue(raw)
		if value == "" {
			continue
		}
		suggestion := Suggestion{
			FieldPath: target.FieldPath,
			Slot:      target.Slot,
			Value:     value,
			Reason:    "llm suggestion from template content generation",
			Source:    firstNonEmpty(strings.TrimSpace(g.source), domain.DraftSuggestionSourceLLM),
		}
		out.Suggestions = append(out.Suggestions, suggestion)
		out.ByFieldPath[target.FieldPath] = suggestion
	}
	mappingLogger().InfoContext(ctx, "llm suggestion request",
		"component", "autofill",
		"step", step,
		"status", "success",
		"source", source,
		"draft_id", strings.TrimSpace(req.DraftID),
		"template_id", req.TemplateID,
		"suggestion_count", len(out.Suggestions),
		"duration_ms", time.Since(started).Milliseconds(),
	)
	return out, nil
}

type CompositeSuggestionGenerator struct {
	Primary  SuggestionGenerator
	Fallback SuggestionGenerator
}

func NewCompositeSuggestionGenerator(primary, fallback SuggestionGenerator) *CompositeSuggestionGenerator {
	return &CompositeSuggestionGenerator{
		Primary:  primary,
		Fallback: fallback,
	}
}

func (g *CompositeSuggestionGenerator) Generate(ctx context.Context, req SuggestionRequest) (SuggestionResult, error) {
	started := time.Now()
	if g == nil {
		return SuggestionResult{}, fmt.Errorf("suggestion generator is not configured")
	}
	if g.Primary == nil {
		mappingLogger().InfoContext(ctx, "autofill fallback",
			"component", "autofill",
			"step", "fallback_attempt",
			"status", "attempted",
			"fallback_generator", generatorLabel(g.Fallback),
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
		)
		return generateFallback(ctx, g.Fallback, req)
	}

	primaryResult, err := g.Primary.Generate(ctx, req)
	if err != nil {
		mappingLogger().WarnContext(ctx, "autofill fallback",
			"component", "autofill",
			"step", "primary_failed",
			"status", "failed",
			"primary_generator", generatorLabel(g.Primary),
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"error", shortErr(err),
		)
		mappingLogger().InfoContext(ctx, "autofill fallback",
			"component", "autofill",
			"step", "qc_fallback",
			"status", "attempted",
			"fallback_generator", generatorLabel(g.Fallback),
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
		)
		return generateFallback(ctx, g.Fallback, req)
	}
	primaryResult = normalizeSuggestionResult(primaryResult, req.Fields, req.Existing, req.IncludeFilled)
	if g.Fallback == nil {
		mappingLogger().InfoContext(ctx, "autofill result",
			"component", "autofill",
			"step", "final",
			"status", "success",
			"source_path", "primary_only",
			"suggestion_count", len(primaryResult.Suggestions),
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return primaryResult, nil
	}

	targets := collectSuggestionTargets(req.Fields, req.Existing, req.IncludeFilled)
	missingFieldPaths := missingSuggestionFieldPaths(targets, primaryResult.ByFieldPath)
	if len(missingFieldPaths) == 0 {
		mappingLogger().InfoContext(ctx, "autofill result",
			"component", "autofill",
			"step", "final",
			"status", "success",
			"source_path", "primary_only",
			"suggestion_count", len(primaryResult.Suggestions),
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return primaryResult, nil
	}

	fallbackReq := narrowedSuggestionRequest(req, missingFieldPaths)
	fallbackResult, fbErr := g.Fallback.Generate(ctx, fallbackReq)
	if fbErr != nil {
		mappingLogger().WarnContext(ctx, "autofill fallback",
			"component", "autofill",
			"step", "qc_fallback",
			"status", "failed",
			"fallback_generator", generatorLabel(g.Fallback),
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"missing_target_count", len(missingFieldPaths),
			"error", shortErr(fbErr),
		)
		mappingLogger().InfoContext(ctx, "autofill result",
			"component", "autofill",
			"step", "final",
			"status", "success",
			"source_path", "primary_only_fallback_failed",
			"suggestion_count", len(primaryResult.Suggestions),
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return primaryResult, nil
	}
	mappingLogger().InfoContext(ctx, "autofill fallback",
		"component", "autofill",
		"step", "qc_fallback",
		"status", "success",
		"fallback_generator", generatorLabel(g.Fallback),
		"draft_id", strings.TrimSpace(req.DraftID),
		"template_id", req.TemplateID,
		"missing_target_count", len(missingFieldPaths),
		"suggestion_count", len(fallbackResult.Suggestions),
	)
	fallbackResult = normalizeSuggestionResult(fallbackResult, fallbackReq.Fields, fallbackReq.Existing, fallbackReq.IncludeFilled)
	merged := primaryResult
	if merged.ByFieldPath == nil {
		merged.ByFieldPath = map[string]Suggestion{}
	}
	for _, suggestion := range fallbackResult.Suggestions {
		if _, exists := merged.ByFieldPath[suggestion.FieldPath]; exists {
			continue
		}
		merged.Suggestions = append(merged.Suggestions, suggestion)
		merged.ByFieldPath[suggestion.FieldPath] = suggestion
	}
	sort.SliceStable(merged.Suggestions, func(i, j int) bool {
		return merged.Suggestions[i].FieldPath < merged.Suggestions[j].FieldPath
	})
	sourcePath := "primary_plus_fallback"
	if len(primaryResult.Suggestions) == 0 && len(fallbackResult.Suggestions) > 0 {
		sourcePath = "fallback_only"
	}
	ruleBasedCount := 0
	for _, suggestion := range merged.Suggestions {
		if strings.EqualFold(strings.TrimSpace(suggestion.Source), domain.DraftSuggestionSourceFallbackRuleBased) {
			ruleBasedCount++
		}
	}
	if ruleBasedCount > 0 {
		mappingLogger().InfoContext(ctx, "autofill fallback",
			"component", "autofill",
			"step", "rule_based_fallback",
			"status", "used",
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"suggestion_count", ruleBasedCount,
		)
	}
	mappingLogger().InfoContext(ctx, "autofill result",
		"component", "autofill",
		"step", "final",
		"status", "success",
		"source_path", sourcePath,
		"suggestion_count", len(merged.Suggestions),
		"draft_id", strings.TrimSpace(req.DraftID),
		"template_id", req.TemplateID,
		"duration_ms", time.Since(started).Milliseconds(),
	)
	return merged, nil
}

func missingSuggestionFieldPaths(targets []SemanticSlotTarget, byFieldPath map[string]Suggestion) []string {
	if byFieldPath == nil {
		byFieldPath = map[string]Suggestion{}
	}
	missing := make([]string, 0, len(targets))
	seen := map[string]struct{}{}
	for _, target := range targets {
		path := strings.TrimSpace(target.FieldPath)
		if path == "" {
			continue
		}
		if _, ok := seen[path]; ok {
			continue
		}
		seen[path] = struct{}{}
		if _, ok := byFieldPath[path]; ok {
			continue
		}
		missing = append(missing, path)
	}
	return missing
}

func narrowedSuggestionRequest(req SuggestionRequest, targetFieldPaths []string) SuggestionRequest {
	allowed := map[string]struct{}{}
	for _, path := range targetFieldPaths {
		trimmed := strings.TrimSpace(path)
		if trimmed == "" {
			continue
		}
		allowed[trimmed] = struct{}{}
	}
	filteredFields := make([]domain.TemplateField, 0, len(req.Fields))
	for _, field := range req.Fields {
		if _, ok := allowed[strings.TrimSpace(field.Path)]; !ok {
			continue
		}
		filteredFields = append(filteredFields, field)
	}
	filteredExisting := make(map[string]string, len(req.Existing))
	for path, value := range req.Existing {
		if _, ok := allowed[strings.TrimSpace(path)]; !ok {
			continue
		}
		filteredExisting[path] = value
	}
	req.Fields = filteredFields
	req.Existing = filteredExisting
	return req
}

func generateFallback(ctx context.Context, fallback SuggestionGenerator, req SuggestionRequest) (SuggestionResult, error) {
	if fallback == nil {
		return SuggestionResult{}, fmt.Errorf("fallback suggestion generator is not configured")
	}
	started := time.Now()
	label := generatorLabel(fallback)
	mappingLogger().InfoContext(ctx, "autofill fallback",
		"component", "autofill",
		"step", "fallback_attempt",
		"status", "attempted",
		"fallback_generator", label,
		"draft_id", strings.TrimSpace(req.DraftID),
		"template_id", req.TemplateID,
	)
	result, err := fallback.Generate(ctx, req)
	if err != nil {
		mappingLogger().WarnContext(ctx, "autofill fallback",
			"component", "autofill",
			"step", "fallback_attempt",
			"status", "failed",
			"fallback_generator", label,
			"draft_id", strings.TrimSpace(req.DraftID),
			"template_id", req.TemplateID,
			"error", shortErr(err),
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, err
	}
	mappingLogger().InfoContext(ctx, "autofill fallback",
		"component", "autofill",
		"step", "fallback_attempt",
		"status", "success",
		"fallback_generator", label,
		"draft_id", strings.TrimSpace(req.DraftID),
		"template_id", req.TemplateID,
		"suggestion_count", len(result.Suggestions),
		"duration_ms", time.Since(started).Milliseconds(),
	)
	return normalizeSuggestionResult(result, req.Fields, req.Existing, req.IncludeFilled), nil
}

func normalizeSuggestionResult(result SuggestionResult, fields []domain.TemplateField, existing map[string]string, includeFilled bool) SuggestionResult {
	allowed := make(map[string]SemanticSlotTarget)
	for _, target := range collectSuggestionTargets(fields, existing, includeFilled) {
		if _, exists := allowed[target.FieldPath]; exists {
			continue
		}
		allowed[target.FieldPath] = target
	}
	out := SuggestionResult{
		Suggestions: make([]Suggestion, 0, len(result.Suggestions)),
		ByFieldPath: map[string]Suggestion{},
	}
	for _, suggestion := range result.Suggestions {
		fieldPath := strings.TrimSpace(suggestion.FieldPath)
		if fieldPath == "" {
			continue
		}
		target, ok := allowed[fieldPath]
		if !ok {
			continue
		}
		value := strings.TrimSpace(suggestion.Value)
		if value == "" {
			continue
		}
		normalized := suggestion
		normalized.FieldPath = fieldPath
		if strings.TrimSpace(normalized.Slot) == "" {
			normalized.Slot = target.Slot
		}
		normalized.Value = value
		normalized.Source = strings.TrimSpace(normalized.Source)
		if _, exists := out.ByFieldPath[fieldPath]; exists {
			continue
		}
		out.Suggestions = append(out.Suggestions, normalized)
		out.ByFieldPath[fieldPath] = normalized
	}
	sort.SliceStable(out.Suggestions, func(i, j int) bool {
		return out.Suggestions[i].FieldPath < out.Suggestions[j].FieldPath
	})
	return out
}

func collectSuggestionTargets(fields []domain.TemplateField, existing map[string]string, includeFilled bool) []SemanticSlotTarget {
	normalizedExisting := existing
	if normalizedExisting == nil {
		normalizedExisting = map[string]string{}
	}
	mappingResult := MapTemplateFieldsToSemanticSlots(fields)
	targets := append([]SemanticSlotTarget(nil), mappingResult.Targets...)
	sort.SliceStable(targets, func(i, j int) bool {
		if targets[i].FieldPath == targets[j].FieldPath {
			return targets[i].Slot < targets[j].Slot
		}
		return targets[i].FieldPath < targets[j].FieldPath
	})

	out := make([]SemanticSlotTarget, 0, len(targets))
	seen := map[string]struct{}{}
	for _, target := range targets {
		if _, exists := seen[target.FieldPath]; exists {
			continue
		}
		if !includeFilled && strings.TrimSpace(normalizedExisting[target.FieldPath]) != "" {
			continue
		}
		out = append(out, target)
		seen[target.FieldPath] = struct{}{}
	}
	return out
}

func enabledPromptBlocks(blocks []domain.PromptBlockConfig) []map[string]string {
	out := make([]map[string]string, 0, len(blocks))
	for _, block := range blocks {
		if !block.Enabled {
			continue
		}
		entry := map[string]string{"id": strings.TrimSpace(block.ID)}
		if label := strings.TrimSpace(block.Label); label != "" {
			entry["label"] = label
		}
		if instruction := strings.TrimSpace(block.Instruction); instruction != "" {
			entry["instruction"] = instruction
		}
		out = append(out, entry)
	}
	return out
}

func llmDraftContextMap(ctx *domain.DraftContext) map[string]any {
	if ctx == nil {
		return map[string]any{}
	}
	return map[string]any{
		"businessType":   strings.TrimSpace(ctx.LLM.BusinessType),
		"websiteUrl":     strings.TrimSpace(ctx.LLM.WebsiteURL),
		"websiteSummary": strings.TrimSpace(ctx.LLM.WebsiteSummary),
		"styleProfile": map[string]string{
			"localeStyle":        strings.TrimSpace(ctx.LLM.StyleProfile.LocaleStyle),
			"marketStyle":        strings.TrimSpace(ctx.LLM.StyleProfile.MarketStyle),
			"addressMode":        strings.TrimSpace(ctx.LLM.StyleProfile.AddressMode),
			"contentTone":        strings.TrimSpace(ctx.LLM.StyleProfile.ContentTone),
			"promptInstructions": strings.TrimSpace(ctx.LLM.StyleProfile.PromptInstructions),
		},
	}
}

func contentTone(ctx *domain.DraftContext) string {
	if ctx == nil {
		return ""
	}
	return strings.TrimSpace(ctx.LLM.StyleProfile.ContentTone)
}

func targetAudience(req SuggestionRequest) string {
	ctx := suggestionContextFrom(req.GlobalData, req.DraftContext)
	parts := make([]string, 0, 4)
	if ctx.BusinessType != "" {
		parts = append(parts, "businessType="+ctx.BusinessType)
	}
	if ctx.LocaleStyle != "" {
		parts = append(parts, "locale="+ctx.LocaleStyle)
	}
	if ctx.MarketStyle != "" {
		parts = append(parts, "market="+ctx.MarketStyle)
	}
	if ctx.AddressMode != "" {
		parts = append(parts, "addressMode="+ctx.AddressMode)
	}
	return strings.Join(parts, ", ")
}

func normalizeLLMValue(raw any) string {
	switch value := raw.(type) {
	case string:
		return strings.TrimSpace(value)
	default:
		return ""
	}
}
