package mapping

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"sort"
	"strings"
	"time"

	"qctextbuilder/internal/domain"
	"qctextbuilder/internal/llmruntime"
)

type SettingsReader interface {
	GetSettings(ctx context.Context) (*domain.AppSettings, error)
}

type ProviderAwareSuggestionGenerator struct {
	settings       SettingsReader
	runtimeFactory *llmruntime.Factory
}

func NewProviderAwareSuggestionGenerator(settings SettingsReader, runtimeFactory *llmruntime.Factory) *ProviderAwareSuggestionGenerator {
	return &ProviderAwareSuggestionGenerator{
		settings:       settings,
		runtimeFactory: runtimeFactory,
	}
}

func (g *ProviderAwareSuggestionGenerator) Generate(ctx context.Context, req SuggestionRequest) (SuggestionResult, error) {
	started := time.Now()
	if g == nil || g.settings == nil || g.runtimeFactory == nil {
		mappingLogger().WarnContext(ctx, "provider-aware suggestion",
			"component", "autofill",
			"step", "provider_aware_config",
			"status", "failed",
			"template_id", req.TemplateID,
			"error", "provider-aware generator is not configured",
		)
		return SuggestionResult{}, fmt.Errorf("provider-aware generator is not configured")
	}

	settings, err := g.settings.GetSettings(ctx)
	if err != nil || settings == nil {
		mappingLogger().WarnContext(ctx, "provider-aware suggestion",
			"component", "autofill",
			"step", "provider_aware_settings",
			"status", "failed",
			"template_id", req.TemplateID,
			"error", "llm settings are not available",
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, fmt.Errorf("llm settings are not available")
	}
	provider := domain.NormalizeLLMProvider(settings.LLMActiveProvider)
	model := domain.NormalizeLLMModel(provider, settings.LLMActiveModel)
	baseURL := strings.TrimSpace(settings.LLMBaseURL)
	mappingLogger().InfoContext(ctx, "provider-aware suggestion",
		"component", "autofill",
		"step", "provider_aware_request",
		"status", "start",
		"provider", provider,
		"model", model,
		"template_id", req.TemplateID,
		"draft_id", strings.TrimSpace(req.DraftID),
	)
	if strings.TrimSpace(model) == "" {
		mappingLogger().WarnContext(ctx, "provider-aware suggestion",
			"component", "autofill",
			"step", "provider_aware_request",
			"status", "failed",
			"provider", provider,
			"model", model,
			"template_id", req.TemplateID,
			"draft_id", strings.TrimSpace(req.DraftID),
			"error", "no active model configured",
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, fmt.Errorf("no active model configured")
	}
	apiKey := domain.LLMAPIKeyForProvider(provider, *settings)
	if provider != domain.LLMProviderOllama && strings.TrimSpace(apiKey) == "" {
		mappingLogger().WarnContext(ctx, "provider-aware suggestion",
			"component", "autofill",
			"step", "provider_aware_request",
			"status", "failed",
			"provider", provider,
			"model", model,
			"template_id", req.TemplateID,
			"draft_id", strings.TrimSpace(req.DraftID),
			"error", "missing api key",
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, fmt.Errorf("api key for provider %s is not configured in settings", provider)
	}

	targets := collectSuggestionTargets(req.Fields, req.Existing, req.IncludeFilled)
	if len(targets) == 0 {
		return SuggestionResult{Suggestions: []Suggestion{}, ByFieldPath: map[string]Suggestion{}}, nil
	}
	allowed := make(map[string]SemanticSlotTarget, len(targets))
	for _, target := range targets {
		allowed[target.FieldPath] = target
	}

	providerClient, err := g.runtimeFactory.ClientFor(provider)
	if err != nil {
		mappingLogger().WarnContext(ctx, "provider-aware suggestion",
			"component", "autofill",
			"step", "provider_aware_request",
			"status", "failed",
			"provider", provider,
			"model", model,
			"template_id", req.TemplateID,
			"draft_id", strings.TrimSpace(req.DraftID),
			"error", shortErr(err),
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, err
	}
	systemPrompt, userPrompt := buildProviderPrompts(req, targets)
	temperature := domain.NormalizeLLMTemperature(settings.LLMTemperature)
	maxTokens := domain.NormalizeLLMMaxTokens(settings.LLMMaxTokens)
	mappingLogger().DebugContext(ctx, "provider-aware suggestion",
		"component", "autofill",
		"step", "provider_aware_request_payload",
		"status", "start",
		"provider", provider,
		"model", model,
		"template_id", req.TemplateID,
		"draft_id", strings.TrimSpace(req.DraftID),
		"base_url", llmruntimeSafeBaseURL(baseURL),
		"system_prompt_chars", len(systemPrompt),
		"system_prompt_snippet", providerLogSnippet(systemPrompt, 1500),
		"user_prompt_chars", len(userPrompt),
		"user_prompt_snippet", providerLogSnippet(userPrompt, 2000),
	)
	raw, err := providerClient.Generate(ctx, llmruntime.Request{
		Provider:     provider,
		Model:        model,
		BaseURL:      baseURL,
		APIKey:       strings.TrimSpace(apiKey),
		Temperature:  &temperature,
		MaxTokens:    &maxTokens,
		SystemPrompt: systemPrompt,
		UserPrompt:   userPrompt,
	})
	if err != nil {
		mappingLogger().WarnContext(ctx, "provider-aware suggestion",
			"component", "autofill",
			"step", "provider_aware_request",
			"status", "failed",
			"provider", provider,
			"model", model,
			"template_id", req.TemplateID,
			"draft_id", strings.TrimSpace(req.DraftID),
			"error", shortErr(err),
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, fmt.Errorf("provider request failed (provider=%s model=%s): %w", provider, model, err)
	}
	mappingLogger().InfoContext(ctx, "provider-aware suggestion",
		"component", "autofill",
		"step", "provider_aware_request",
		"status", "success",
		"provider", provider,
		"model", model,
		"template_id", req.TemplateID,
		"draft_id", strings.TrimSpace(req.DraftID),
		"response_chars", len(strings.TrimSpace(raw)),
	)
	mappingLogger().DebugContext(ctx, "provider-aware suggestion",
		"component", "autofill",
		"step", "provider_aware_request",
		"status", "success",
		"provider", provider,
		"model", model,
		"template_id", req.TemplateID,
		"draft_id", strings.TrimSpace(req.DraftID),
		"response_snippet", providerLogSnippet(raw, 4000),
	)
	mappingLogger().DebugContext(ctx, "provider-aware suggestion",
		"component", "autofill",
		"step", "provider_parse_input",
		"status", "start",
		"provider", provider,
		"model", model,
		"template_id", req.TemplateID,
		"draft_id", strings.TrimSpace(req.DraftID),
		"extracted_content_chars", len(strings.TrimSpace(raw)),
		"extracted_content_snippet", providerLogSnippet(raw, 4000),
	)

	parsed, err := parseProviderSuggestions(raw)
	if err != nil {
		mappingLogger().WarnContext(ctx, "provider-aware suggestion",
			"component", "autofill",
			"step", "provider_parse",
			"status", "failed",
			"provider", provider,
			"model", model,
			"template_id", req.TemplateID,
			"draft_id", strings.TrimSpace(req.DraftID),
			"error", shortErr(err),
			"duration_ms", time.Since(started).Milliseconds(),
		)
		return SuggestionResult{}, fmt.Errorf("provider returned invalid suggestions json (provider=%s model=%s): %w", provider, model, err)
	}
	mappingLogger().InfoContext(ctx, "provider-aware suggestion",
		"component", "autofill",
		"step", "provider_parse",
		"status", "success",
		"provider", provider,
		"model", model,
		"template_id", req.TemplateID,
		"draft_id", strings.TrimSpace(req.DraftID),
		"parsed_count", len(parsed),
	)
	mappingLogger().DebugContext(ctx, "provider-aware suggestion",
		"component", "autofill",
		"step", "provider_parse",
		"status", "success",
		"provider", provider,
		"model", model,
		"template_id", req.TemplateID,
		"draft_id", strings.TrimSpace(req.DraftID),
		"parsed_sample", providerSuggestionSample(parsed, 5),
	)

	out := SuggestionResult{
		Suggestions: make([]Suggestion, 0, len(parsed)),
		ByFieldPath: map[string]Suggestion{},
	}
	for _, item := range parsed {
		fieldPath := strings.TrimSpace(item.FieldPath)
		target, ok := allowed[fieldPath]
		if !ok {
			continue
		}
		value := strings.TrimSpace(item.Value)
		if value == "" {
			continue
		}
		suggestion := Suggestion{
			FieldPath: fieldPath,
			Slot:      firstNonEmpty(strings.TrimSpace(item.Slot), target.Slot),
			Value:     value,
			Reason:    firstNonEmpty(strings.TrimSpace(item.Reason), "provider suggestion"),
			Source:    provider,
		}
		if _, exists := out.ByFieldPath[fieldPath]; exists {
			continue
		}
		out.Suggestions = append(out.Suggestions, suggestion)
		out.ByFieldPath[fieldPath] = suggestion
	}
	mappingLogger().InfoContext(ctx, "provider-aware suggestion",
		"component", "autofill",
		"step", "provider_aware_result",
		"status", "success",
		"provider", provider,
		"model", model,
		"template_id", req.TemplateID,
		"draft_id", strings.TrimSpace(req.DraftID),
		"suggestion_count", len(out.Suggestions),
		"sources", summarizeResultSources(out),
		"duration_ms", time.Since(started).Milliseconds(),
	)
	mappingLogger().DebugContext(ctx, "provider-aware suggestion",
		"component", "autofill",
		"step", "provider_aware_result",
		"status", "success",
		"provider", provider,
		"model", model,
		"template_id", req.TemplateID,
		"draft_id", strings.TrimSpace(req.DraftID),
		"suggestion_sample_sources", sampleResultSources(out, 5),
		"suggestion_sample", suggestionLogSample(out, 5),
	)
	return out, nil
}

type providerSuggestion struct {
	FieldPath string `json:"fieldPath"`
	Slot      string `json:"slot,omitempty"`
	Value     string `json:"value"`
	Reason    string `json:"reason,omitempty"`
}

func parseProviderSuggestions(raw string) ([]providerSuggestion, error) {
	content := strings.TrimSpace(raw)
	if content == "" {
		return nil, fmt.Errorf("empty provider response")
	}

	candidates := []string{content}
	if fence := extractFencedJSON(content); fence != "" {
		candidates = append([]string{fence}, candidates...)
	}
	if object := extractJSONObject(content); object != "" {
		candidates = append(candidates, object)
	}

	var firstErr error
	for _, candidate := range candidates {
		items, err := parseSuggestionsCandidate(candidate)
		if err == nil {
			return items, nil
		}
		if firstErr == nil {
			firstErr = err
		}
	}
	if firstErr != nil {
		return nil, firstErr
	}
	return nil, fmt.Errorf("provider response is not valid suggestions json")
}

func parseSuggestionsCandidate(raw string) ([]providerSuggestion, error) {
	var root any
	if err := json.Unmarshal([]byte(raw), &root); err != nil {
		return nil, fmt.Errorf("provider response is not valid json: %w", err)
	}

	var itemsRaw []any
	switch value := root.(type) {
	case map[string]any:
		suggestions, ok := value["suggestions"]
		if !ok {
			return nil, fmt.Errorf("provider json object must contain \"suggestions\" array")
		}
		list, ok := suggestions.([]any)
		if !ok {
			return nil, fmt.Errorf("provider \"suggestions\" must be an array")
		}
		itemsRaw = list
	case []any:
		itemsRaw = value
	default:
		return nil, fmt.Errorf("provider json payload must be an object or array")
	}
	if len(itemsRaw) == 0 {
		return nil, fmt.Errorf("provider returned an empty suggestions array")
	}

	out := make([]providerSuggestion, 0, len(itemsRaw))
	for idx, rawItem := range itemsRaw {
		itemMap, ok := rawItem.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("suggestion #%d is not an object", idx+1)
		}
		fieldPath := strings.TrimSpace(anyToString(itemMap["fieldPath"]))
		if fieldPath == "" {
			return nil, fmt.Errorf("suggestion #%d has empty fieldPath", idx+1)
		}
		value := strings.TrimSpace(anyToString(itemMap["value"]))
		if value == "" {
			return nil, fmt.Errorf("suggestion #%d for fieldPath %q has empty value", idx+1, fieldPath)
		}
		out = append(out, providerSuggestion{
			FieldPath: fieldPath,
			Slot:      strings.TrimSpace(anyToString(itemMap["slot"])),
			Value:     value,
			Reason:    strings.TrimSpace(anyToString(itemMap["reason"])),
		})
	}
	return out, nil
}

func extractFencedJSON(value string) string {
	const fence = "```"
	start := strings.Index(value, fence)
	for start >= 0 {
		rest := value[start+len(fence):]
		end := strings.Index(rest, fence)
		if end < 0 {
			return ""
		}
		block := strings.TrimSpace(rest[:end])
		block = strings.TrimPrefix(block, "json")
		block = strings.TrimPrefix(block, "JSON")
		block = strings.TrimSpace(block)
		if strings.HasPrefix(block, "{") || strings.HasPrefix(block, "[") {
			return block
		}
		nextOffset := start + len(fence) + end + len(fence)
		nextStart := strings.Index(value[nextOffset:], fence)
		if nextStart < 0 {
			break
		}
		start = nextOffset + nextStart
	}
	return ""
}

func extractJSONObject(value string) string {
	start := strings.IndexAny(value, "{[")
	if start < 0 {
		return ""
	}
	end := strings.LastIndexAny(value, "}]")
	if end <= start {
		return ""
	}
	return strings.TrimSpace(value[start : end+1])
}

func buildProviderPrompts(req SuggestionRequest, targets []SemanticSlotTarget) (string, string) {
	targetPayload := make([]map[string]string, 0, len(targets))
	for _, target := range targets {
		targetPayload = append(targetPayload, map[string]string{
			"fieldPath": strings.TrimSpace(target.FieldPath),
			"slot":      strings.TrimSpace(target.Slot),
		})
	}
	contextPayload := map[string]any{
		"globalData":   req.GlobalData,
		"draftContext": llmDraftContextMap(req.DraftContext),
		"masterPrompt": strings.TrimSpace(req.MasterPrompt),
		"promptBlocks": enabledPromptBlocks(req.PromptBlocks),
		"targets":      targetPayload,
	}
	contextJSON, _ := json.MarshalIndent(contextPayload, "", "  ")

	system := "You generate website text suggestions. Return JSON only. Format: {\"suggestions\":[{\"fieldPath\":\"...\",\"slot\":\"...\",\"value\":\"...\",\"reason\":\"...\"}]}. Use only provided field paths. Keep values concise and in input language."
	user := "Generate suggestions for each target field using the provided context. Do not include markdown.\n\n" + string(contextJSON)
	return system, user
}

func anyToString(raw any) string {
	switch value := raw.(type) {
	case string:
		return value
	case float64:
		return fmt.Sprintf("%.0f", value)
	case bool:
		if value {
			return "true"
		}
		return "false"
	case nil:
		return ""
	default:
		return fmt.Sprintf("%v", value)
	}
}

func providerLogSnippet(value string, limit int) string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" || limit <= 0 {
		return ""
	}
	runes := []rune(trimmed)
	if len(runes) <= limit {
		return trimmed
	}
	return strings.TrimSpace(string(runes[:limit])) + "...(truncated)"
}

func providerSuggestionSample(items []providerSuggestion, limit int) []map[string]string {
	if len(items) == 0 || limit <= 0 {
		return []map[string]string{}
	}
	if len(items) > limit {
		items = items[:limit]
	}
	out := make([]map[string]string, 0, len(items))
	for _, item := range items {
		out = append(out, map[string]string{
			"fieldPath": strings.TrimSpace(item.FieldPath),
			"slot":      strings.TrimSpace(item.Slot),
			"value":     providerLogSnippet(item.Value, 200),
			"reason":    providerLogSnippet(item.Reason, 120),
		})
	}
	return out
}

func suggestionLogSample(result SuggestionResult, limit int) []map[string]string {
	if limit <= 0 || len(result.ByFieldPath) == 0 {
		return []map[string]string{}
	}
	paths := make([]string, 0, len(result.ByFieldPath))
	for path := range result.ByFieldPath {
		paths = append(paths, path)
	}
	sort.Strings(paths)
	if len(paths) > limit {
		paths = paths[:limit]
	}
	out := make([]map[string]string, 0, len(paths))
	for _, path := range paths {
		item := result.ByFieldPath[path]
		out = append(out, map[string]string{
			"fieldPath": strings.TrimSpace(item.FieldPath),
			"source":    strings.TrimSpace(item.Source),
			"slot":      strings.TrimSpace(item.Slot),
			"value":     providerLogSnippet(item.Value, 200),
		})
	}
	return out
}

func llmruntimeSafeBaseURL(value string) string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return ""
	}
	parsed, err := url.Parse(trimmed)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return trimmed
	}
	parsed.User = nil
	parsed.RawQuery = ""
	parsed.Fragment = ""
	return strings.TrimRight(parsed.String(), "/")
}
