package job

import (
	"context"
	"fmt"
	"strings"

	libbrave "haixun-backend/internal/library/brave"
	"haixun-backend/internal/library/clock"
	app "haixun-backend/internal/library/errors"
	"haixun-backend/internal/library/errors/code"
	libkg "haixun-backend/internal/library/knowledge"
	"haixun-backend/internal/library/placement"
	libprompt "haixun-backend/internal/library/prompt"
	"haixun-backend/internal/model/ai/domain/enum"
	domai "haixun-backend/internal/model/ai/domain/usecase"
	aiusecase "haixun-backend/internal/model/ai/usecase"
	brandentity "haixun-backend/internal/model/brand/domain/entity"
	branddomain "haixun-backend/internal/model/brand/domain/usecase"
	jobdom "haixun-backend/internal/model/job/domain/usecase"
	kgusecase "haixun-backend/internal/model/knowledge_graph/domain/usecase"
	placementusecase "haixun-backend/internal/model/placement/usecase"
	threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase"
)

type ExpandGraphDeps struct {
	Jobs           jobdom.UseCase
	Brand          branddomain.UseCase
	KnowledgeGraph kgusecase.UseCase
	ThreadsAccount threadsaccountdomain.UseCase
	Placement      placementusecase.UseCase
	AI             aiusecase.UseCase
}

func RegisterExpandGraphHandler(runner *Runner, deps ExpandGraphDeps) {
	if runner == nil {
		return
	}
	runner.RegisterStepHandler("expand", func(ctx context.Context, step StepContext) error {
		return runExpandGraph(ctx, step, deps)
	})
}

func brandIDFromPayload(payload map[string]any) string {
	brandID := stringField(payload, "brand_id")
	if brandID == "" {
		brandID = stringField(payload, "persona_id")
	}
	return brandID
}

func runExpandGraph(ctx context.Context, step StepContext, deps ExpandGraphDeps) error {
	payload := step.Run.Payload
	tenantID := stringField(payload, "tenant_id")
	ownerUID := stringField(payload, "owner_uid")
	brandID := brandIDFromPayload(payload)
	seed := stringField(payload, "seed_query")
	supplemental := boolField(payload, "supplemental")

	if tenantID == "" || ownerUID == "" || brandID == "" {
		return fmt.Errorf("expand-graph payload missing tenant_id, owner_uid, or brand_id")
	}
	if seed == "" {
		return fmt.Errorf("expand-graph payload missing seed_query")
	}

	brand, err := deps.Brand.Get(ctx, tenantID, ownerUID, brandID)
	if err != nil {
		return err
	}

	productBrief := strings.TrimSpace(brand.ProductBrief)
	if formatted := placement.ProductBriefFromContext(brand.ProductContext); formatted != "" {
		productBrief = formatted
	}

	research, err := deps.Placement.ResearchSettings(ctx, tenantID, ownerUID)
	if err != nil {
		return err
	}
	memberCtx, err := deps.ThreadsAccount.ResolveMemberPlacementContext(ctx, tenantID, ownerUID, research)
	if err != nil {
		return err
	}
	braveClient := libbrave.NewClient(memberCtx.BraveAPIKey)

	credential, err := deps.ThreadsAccount.ResolveMemberAiCredential(ctx, tenantID, ownerUID)
	if err != nil {
		return err
	}
	providerID, err := aiusecase.MapWorkerProvider(credential.Provider)
	if err != nil {
		return err
	}

	updateProgress := func(summary string, percentage int) {
		_ = step.Heartbeat(ctx)
		_, _ = deps.Jobs.UpdateProgress(ctx, jobdom.UpdateProgressRequest{
			JobID:      step.JobID,
			WorkerID:   step.WorkerID,
			Phase:      "expand",
			Summary:    summary,
			Percentage: percentage,
		})
	}

	bootstrap := boolField(payload, "bootstrap")
	if bootstrap || brand.ResearchMap.IsEmpty() {
		updateProgress("產生研究地圖…", 5)
		if err := ensureResearchMap(ctx, step, deps, brand, productBrief, providerID, credential, updateProgress); err != nil {
			return err
		}
		brand, err = deps.Brand.Get(ctx, tenantID, ownerUID, brandID)
		if err != nil {
			return err
		}
	}

	updateProgress("規劃 Brave 查詢…", 10)

	var existing *kgusecase.GraphSummary
	if supplemental {
		existing, _ = deps.KnowledgeGraph.Get(ctx, tenantID, ownerUID, brandID)
	}

	l1Labels := []string{}
	if existing != nil {
		l1Labels = libkg.L1LabelsFromNodes(existing.Nodes)
	}
	queries := libkg.PlanQueries(libkg.PlanInput{
		Seed:           seed,
		TargetAudience: brand.TargetAudience,
		ProductBrief:   productBrief,
		L1Labels:       l1Labels,
		Supplemental:   supplemental,
	})

	updateProgress(fmt.Sprintf("Brave 知識擴展（%d 查詢）…", len(queries)), 25)

	braveSources, err := runBraveKnowledgeExpand(ctx, braveClient, memberCtx, queries, func(i, total int) {
		pct := 25 + ((i + 1) * 30 / max(total, 1))
		updateProgress(fmt.Sprintf("Brave 查詢進行中 %d/%d…", i+1, total), pct)
	}, func() error {
		cancelled, _ := deps.Jobs.IsCancelRequested(ctx, step.JobID)
		if cancelled {
			return errJobCancelled
		}
		return ctx.Err()
	})
	if err != nil {
		return err
	}

	updateProgress("AI 合成知識圖譜…", 60)

	systemPrompt, err := libprompt.KnowledgeGraphSystem()
	if err != nil {
		return app.For(code.AI).SysInternal("knowledge graph prompt load failed")
	}
	userPrompt, err := libkg.BuildUserPrompt(libkg.SynthInput{
		Seed:           seed,
		ProductBrief:   productBrief,
		TargetAudience: brand.TargetAudience,
		Persona:        brand.Brief,
		Sources:        braveSources,
	})
	if err != nil {
		return app.For(code.AI).SysInternal("knowledge graph user prompt load failed")
	}
	result, err := deps.AI.GenerateText(ctx, domai.GenerateRequest{
		Provider: providerID,
		Model:    credential.Model,
		Credential: domai.Credential{
			APIKey: credential.APIKey,
		},
		System: systemPrompt,
		Messages: []domai.Message{
			{Role: "user", Content: userPrompt},
		},
	})
	if err != nil {
		return err
	}

	graph, err := libkg.ParseSynthOutput(result.Text, libkg.SynthInput{
		Seed:           seed,
		ProductBrief:   productBrief,
		TargetAudience: brand.TargetAudience,
	}, braveSources)
	if err != nil {
		return app.For(code.AI).SvcThirdParty("知識圖譜 LLM 回傳無法解析：" + err.Error())
	}

	if supplemental && existing != nil {
		graph = mergeGraphs(existing, graph, braveSources)
	}

	needsSupplemental := graph.PainTagCount < libkg.MinPainTagCandidates() && !supplemental
	if needsSupplemental {
		updateProgress(fmt.Sprintf("痛點 tag 僅 %d，執行補充查詢…", graph.PainTagCount), 75)
		suppQueries := libkg.PlanQueries(libkg.PlanInput{
			Seed:         seed,
			L1Labels:     libkg.L1LabelsFromNodes(graph.Nodes),
			Supplemental: true,
		})
		extraSources, err := runBraveKnowledgeExpand(ctx, braveClient, memberCtx, suppQueries, nil, func() error {
			cancelled, _ := deps.Jobs.IsCancelRequested(ctx, step.JobID)
			if cancelled {
				return errJobCancelled
			}
			return ctx.Err()
		})
		if err != nil {
			return err
		}
		braveSources = append(braveSources, extraSources...)
		suppInstruction, err := libprompt.KnowledgeGraphSupplemental()
		if err != nil {
			return app.For(code.AI).SysInternal("knowledge graph supplemental prompt load failed")
		}
		suppUserPrompt, err := libkg.BuildUserPrompt(libkg.SynthInput{
			Seed:           seed,
			ProductBrief:   productBrief,
			TargetAudience: brand.TargetAudience,
			Persona:        brand.Brief,
			Sources:        braveSources,
		})
		if err != nil {
			return err
		}
		suppResult, err := deps.AI.GenerateText(ctx, domai.GenerateRequest{
			Provider: providerID,
			Model:    credential.Model,
			Credential: domai.Credential{
				APIKey: credential.APIKey,
			},
			System: systemPrompt,
			Messages: []domai.Message{
				{Role: "user", Content: suppUserPrompt + "\n\n" + suppInstruction},
			},
		})
		if err == nil {
			if patched, parseErr := libkg.ParseSynthOutput(suppResult.Text, libkg.SynthInput{Seed: seed}, braveSources); parseErr == nil {
				graph = mergeGraphs(&kgusecase.GraphSummary{
					Seed:  graph.Seed,
					Nodes: graph.Nodes,
					Edges: graph.Edges,
				}, patched, braveSources)
			}
		}
		needsSupplemental = graph.PainTagCount < libkg.MinPainTagCandidates()
	}

	updateProgress("寫入知識圖譜…", 90)

	graph.BraveSources = braveSources
	now := clock.NowUnixNano()
	saved, err := deps.KnowledgeGraph.Upsert(ctx, kgusecase.UpsertRequest{
		TenantID:     tenantID,
		OwnerUID:     ownerUID,
		BrandID:      brandID,
		Seed:         graph.Seed,
		Nodes:        graph.Nodes,
		Edges:        graph.Edges,
		BraveSources: graph.BraveSources,
		PainTagCount: graph.PainTagCount,
		GeneratedAt:  now,
	})
	if err != nil {
		return err
	}

	handoff := map[string]any{
		"flow":                      "placement",
		"brand_id":                  brandID,
		"pain_tag_count":            saved.PainTagCount,
		"summary":                   fmt.Sprintf("圖譜 %d 節點，痛點候選 %d", len(saved.Nodes), saved.PainTagCount),
		"next_route":                "/research?brand=" + brandID,
		"needs_supplemental_expand": needsSupplemental,
		"search_source_mode":        string(memberCtx.SearchSourceMode),
		"dev_mode":                  memberCtx.DevMode,
	}
	_, err = deps.Jobs.CompleteRun(ctx, jobdom.CompleteRunRequest{
		JobID:    step.JobID,
		WorkerID: step.WorkerID,
		Result: map[string]any{
			"graph_id":           saved.ID,
			"seed":               saved.Seed,
			"pain_tag_count":     saved.PainTagCount,
			"node_count":         len(saved.Nodes),
			"search_source_mode": string(memberCtx.SearchSourceMode),
			"handoff":            handoff,
		},
	})
	return err
}

func runBraveKnowledgeExpand(
	ctx context.Context,
	client *libbrave.Client,
	member placement.MemberContext,
	queries []string,
	onProgress func(i, total int),
	heartbeat func() error,
) ([]libkg.BraveSource, error) {
	if client == nil || !client.Enabled() {
		return nil, app.For(code.Setting).InputMissingRequired("請在設定頁設定 Brave Search API key（跟隨此登入帳號）")
	}
	out := make([]libkg.BraveSource, 0, len(queries)*2)
	for i, query := range queries {
		if heartbeat != nil {
			if err := heartbeat(); err != nil {
				return nil, err
			}
		}
		res, _ := client.Search(ctx, libbrave.SearchOptions{
			Query:      query,
			Limit:      3,
			Mode:       libbrave.ModeKnowledgeExpand,
			Country:    member.BraveCountry,
			SearchLang: member.BraveSearchLang,
		})
		for _, item := range res.Results {
			out = append(out, libkg.BraveSource{
				Query:   query,
				Snippet: item.Snippet,
				URL:     item.URL,
				Title:   item.Title,
			})
		}
		if onProgress != nil {
			onProgress(i, len(queries))
		}
	}
	if len(out) == 0 {
		return nil, app.For(code.Setting).SvcThirdParty("Brave 查詢無結果，請確認 API key 或稍後重試")
	}
	return out, nil
}

func mergeGraphs(existing *kgusecase.GraphSummary, incoming libkg.Graph, extraSources []libkg.BraveSource) libkg.Graph {
	if existing == nil {
		return incoming
	}
	merged := libkg.Graph{
		Seed:         existing.Seed,
		Nodes:        append([]libkg.Node{}, existing.Nodes...),
		Edges:        append([]libkg.Edge{}, existing.Edges...),
		BraveSources: append([]libkg.BraveSource{}, existing.BraveSources...),
	}
	seenLabel := map[string]struct{}{}
	for _, node := range merged.Nodes {
		seenLabel[strings.ToLower(strings.TrimSpace(node.Label))] = struct{}{}
	}
	for _, node := range incoming.Nodes {
		key := strings.ToLower(strings.TrimSpace(node.Label))
		if _, ok := seenLabel[key]; ok {
			continue
		}
		seenLabel[key] = struct{}{}
		merged.Nodes = append(merged.Nodes, node)
	}
	edgeSeen := map[string]struct{}{}
	for _, edge := range merged.Edges {
		edgeSeen[edge.From+"->"+edge.To] = struct{}{}
	}
	for _, edge := range incoming.Edges {
		key := edge.From + "->" + edge.To
		if _, ok := edgeSeen[key]; ok {
			continue
		}
		edgeSeen[key] = struct{}{}
		merged.Edges = append(merged.Edges, edge)
	}
	merged.BraveSources = append(merged.BraveSources, extraSources...)
	libkg.DeriveSearchTagsFromGraph(&merged)
	return merged
}

func stringField(payload map[string]any, key string) string {
	if payload == nil {
		return ""
	}
	raw, ok := payload[key]
	if !ok || raw == nil {
		return ""
	}
	switch v := raw.(type) {
	case string:
		return strings.TrimSpace(v)
	default:
		return strings.TrimSpace(fmt.Sprint(v))
	}
}

func boolField(payload map[string]any, key string) bool {
	if payload == nil {
		return false
	}
	raw, ok := payload[key]
	if !ok || raw == nil {
		return false
	}
	switch v := raw.(type) {
	case bool:
		return v
	case string:
		return strings.EqualFold(strings.TrimSpace(v), "true")
	default:
		return false
	}
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func ensureResearchMap(
	ctx context.Context,
	step StepContext,
	deps ExpandGraphDeps,
	brand *branddomain.BrandSummary,
	productBrief string,
	providerID enum.ProviderID,
	credential *threadsaccountdomain.WorkerAiCredential,
	updateProgress func(string, int),
) error {
	tenantID := stringField(step.Run.Payload, "tenant_id")
	ownerUID := stringField(step.Run.Payload, "owner_uid")
	if tenantID == "" || ownerUID == "" || brand == nil {
		return fmt.Errorf("research map: missing actor or brand")
	}

	result, err := deps.AI.GenerateText(ctx, domai.GenerateRequest{
		Provider: providerID,
		Model:    credential.Model,
		Credential: domai.Credential{
			APIKey: credential.APIKey,
		},
		System: placement.BuildResearchMapSystemPrompt(),
		Messages: []domai.Message{
			{
				Role: "user",
				Content: placement.BuildResearchMapUserPrompt(placement.ResearchMapInput{
					Label:          brand.DisplayName,
					SeedQuery:      brand.SeedQuery,
					Brief:          brand.Brief,
					ProductContext: brand.ProductContext,
				}),
			},
		},
	})
	if err != nil {
		return err
	}

	parsed, err := placement.ParseResearchMapOutput(result.Text)
	if err != nil {
		return app.For(code.AI).SvcThirdParty("研究地圖 LLM 回傳無法解析：" + err.Error())
	}

	entityMap := brandentity.ResearchMap{
		AudienceSummary: parsed.AudienceSummary,
		ContentGoal:     parsed.ContentGoal,
		Questions:       parsed.Questions,
		Pillars:         parsed.Pillars,
		Exclusions:      parsed.Exclusions,
	}
	targetAudience := strings.TrimSpace(brand.TargetAudience)
	if targetAudience == "" {
		targetAudience = parsed.AudienceSummary
	}
	patch := branddomain.BrandPatch{
		ResearchMap: &entityMap,
	}
	if targetAudience != "" {
		patch.TargetAudience = &targetAudience
	}
	if strings.TrimSpace(brand.ProductBrief) == "" && productBrief != "" {
		patch.ProductBrief = &productBrief
	}

	_, err = deps.Brand.Update(ctx, branddomain.UpdateRequest{
		TenantID: tenantID,
		OwnerUID: ownerUID,
		BrandID:  brand.ID,
		Patch:    patch,
	})
	if err != nil {
		return err
	}
	updateProgress("研究地圖已就緒", 8)
	return nil
}
