package job

import (
	"context"
	"fmt"
	"strings"

	libbrave "haixun-backend/internal/library/brave"
	libkg "haixun-backend/internal/library/knowledge"
	"haixun-backend/internal/library/placement"
	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"
	scanpostusecase "haixun-backend/internal/model/scan_post/domain/usecase"
	threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase"
)

type ScanPlacementDeps struct {
	Jobs           jobdom.UseCase
	Brand          branddomain.UseCase
	KnowledgeGraph kgusecase.UseCase
	ScanPost       scanpostusecase.UseCase
	ThreadsAccount threadsaccountdomain.UseCase
	Placement      placementusecase.UseCase
}

func RegisterScanPlacementHandler(runner *Runner, deps ScanPlacementDeps) {
	if runner == nil {
		return
	}
	runner.RegisterStepHandler("crawl", func(ctx context.Context, step StepContext) error {
		return runScanPlacement(ctx, step, deps)
	})
}

func runScanPlacement(ctx context.Context, step StepContext, deps ScanPlacementDeps) error {
	payload := step.Run.Payload
	tenantID := stringField(payload, "tenant_id")
	ownerUID := stringField(payload, "owner_uid")
	brandID := brandIDFromPayload(payload)
	graphID := stringField(payload, "graph_id")

	if tenantID == "" || ownerUID == "" || brandID == "" {
		return fmt.Errorf("placement-scan payload missing tenant_id, owner_uid, or brand_id")
	}

	graph, err := deps.KnowledgeGraph.Get(ctx, tenantID, ownerUID, brandID)
	if err != nil {
		return err
	}
	if graphID == "" {
		graphID = graph.ID
	}

	nodes := graph.Nodes
	if ids := stringSliceField(payload, "node_ids"); len(ids) > 0 {
		nodes = filterNodesByIDs(graph.Nodes, ids)
	} else {
		nodes = selectedNodes(graph.Nodes)
	}
	if len(nodes) == 0 {
		return fmt.Errorf("請先在研究頁勾選至少一個節點")
	}

	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
	}
	if !memberCtx.AllowsBrave && !memberCtx.AllowsThreadsAPI && !memberCtx.AllowsCrawler {
		return fmt.Errorf("目前連線模式無法海巡，請確認 Threads API、Brave 或 Chrome Session 設定")
	}
	if memberCtx.AllowsBrave && strings.TrimSpace(memberCtx.BraveAPIKey) == "" {
		return fmt.Errorf("請在設定頁設定 Brave Search API key（跟隨此登入帳號）")
	}
	if memberCtx.DevMode && !memberCtx.BrowserConnected {
		return fmt.Errorf("開發模式需先同步 Chrome Session")
	}

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

	exclusions := []string{}
	if brand, brandErr := deps.Brand.Get(ctx, tenantID, ownerUID, brandID); brandErr == nil && brand != nil {
		exclusions = append(exclusions, brand.ResearchMap.Exclusions...)
	}

	updateProgress("準備置入海巡…", 5)

	braveClient := libbrave.NewClient(memberCtx.BraveAPIKey)
	crawlerFn := makeCrawlerSearchFn(deps, tenantID, ownerUID)
	candidates, err := placement.RunDualTrackDiscover(ctx, placement.DualTrackInput{
		Nodes:      nodes,
		Exclusions: exclusions,
		Member:     memberCtx,
		Client:     braveClient,
		Crawler:    crawlerFn,
	}, updateProgress)
	if err != nil {
		return err
	}

	scrapeReplies := memberCtx.ScrapeReplies
	if v, ok := payload["scrape_replies"].(bool); ok {
		scrapeReplies = v
	} else if memberCtx.ApiConnected && strings.TrimSpace(memberCtx.ThreadsAPIAccessToken) != "" {
		// Formal Threads API mode can fetch replies without browser session.
		scrapeReplies = true
	}
	if scrapeReplies {
		updateProgress("抓取高優先貼文留言…", 88)
		candidates = placement.AttachReplies(ctx, placement.ScrapeRepliesInput{
			Posts:          candidates,
			Member:         memberCtx,
			RepliesPerPost: memberCtx.RepliesPerPost,
		})
	}

	updateProgress(fmt.Sprintf("寫入 %d 篇海巡結果…", len(candidates)), 92)
	count, err := deps.ScanPost.ReplaceFromScan(ctx, scanpostusecase.ReplaceRequest{
		TenantID:  tenantID,
		OwnerUID:  ownerUID,
		BrandID:   brandID,
		GraphID:   graphID,
		ScanJobID: step.JobID,
		Posts:     candidates,
	})
	if err != nil {
		return err
	}

	gold := 0
	recent := 0
	solved := 0
	replyCount := 0
	for _, item := range candidates {
		replyCount += len(item.Replies)
		if item.Priority == "gold" {
			gold++
		}
		if item.Priority == "gold" || item.Priority == "recent" {
			recent++
		}
		if item.SolvedByProduct {
			solved++
		}
	}

	handoff := map[string]any{
		"flow":     "placement",
		"brand_id": brandID,
		"summary": fmt.Sprintf(
			"雙軌海巡完成：%d 篇（gold %d、近期軌 %d、產品可解 %d）",
			count, gold, recent, solved,
		),
		"pain_breakdown": map[string]any{
			"posts":          count,
			"gold":           gold,
			"recent_7d":      recent,
			"solved_by_prod": solved,
			"replies":        replyCount,
		},
		"next_route":                "/outreach?brand=" + brandID,
		"needs_supplemental_expand": false,
		"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{
			"post_count":         count,
			"gold_count":         gold,
			"recent_count":       recent,
			"solved_count":       solved,
			"reply_count":        replyCount,
			"search_source_mode": string(memberCtx.SearchSourceMode),
			"handoff":            handoff,
		},
	})
	return err
}

func selectedNodes(nodes []libkg.Node) []libkg.Node {
	out := make([]libkg.Node, 0, len(nodes))
	for _, node := range nodes {
		if node.SelectedForScan {
			out = append(out, node)
		}
	}
	return out
}

func filterNodesByIDs(nodes []libkg.Node, ids []string) []libkg.Node {
	allowed := map[string]struct{}{}
	for _, id := range ids {
		id = strings.TrimSpace(id)
		if id != "" {
			allowed[id] = struct{}{}
		}
	}
	out := make([]libkg.Node, 0, len(ids))
	for _, node := range nodes {
		if _, ok := allowed[node.ID]; ok {
			out = append(out, node)
		}
	}
	return out
}

func stringSliceField(payload map[string]any, key string) []string {
	if payload == nil {
		return nil
	}
	raw, ok := payload[key]
	if !ok || raw == nil {
		return nil
	}
	switch v := raw.(type) {
	case []string:
		return v
	case []any:
		out := make([]string, 0, len(v))
		for _, item := range v {
			if s, ok := item.(string); ok {
				out = append(out, s)
			}
		}
		return out
	default:
		return nil
	}
}
