package viral

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	"haixun-backend/internal/library/placement"
)

type MissionInspireInput struct {
	PersonaDisplayName  string
	PersonaBrief        string
	PersonaBlock        string
	StyleBenchmark      string
	PersonaAudience     string
	PersonaContentGoal  string
	PersonaQuestions    []string
	PersonaPillars      []string
	RecentMissionLabels []string
	RecentSeedQueries   []string
	AvoidTopics         []string
	ContentDirection    string
	UserKeyword         string
	TrendSnippets       []MissionInspireTrendSnippet
	WebSearchProvider   string
	LLMOnly             bool
}

type MissionInspireTrendSnippet struct {
	Query   string
	Title   string
	Snippet string
	URL     string
}

type MissionInspireThreadsInput struct {
	Keyword        string
	PersonaBrief   string
	StyleBenchmark string
	Pillars        []string
	Questions      []string
	Member         placement.MemberContext
	Crawler        placement.CrawlerSearchFn
}

type MissionInspireOutput struct {
	Label          string
	SeedQuery      string
	Brief          string
	TrendReason    string
	TrendKeywords  []string
	Angles         []string
	Mission        string
	TargetAudience string
	OpeningType    string
	BodyType       string
	Emotion        string
	CtaType        string
	RiskLevel      string
	Avoid          []string
}

func BuildMissionInspireSystemPrompt() string {
	return strings.TrimSpace(`你是 Threads 帳號的內容編輯與 Topic Radar。根據近期 Threads / 社群討論訊號、網路搜尋素材與創作者人設，先產出「可執行的內容計畫」，不要直接寫貼文正文。

規則：
1. 若【近期趨勢訊號】有內容，從中挑一個適合在 Threads 海巡的方向；優先使用社群熱門關鍵字、受眾求助、心得討論、留言常見問題，不要把新聞事件本身當主題
2. 若趨勢訊號為空（未連線網路搜尋），**必須**改依人設、受眾痛點與常見 Threads 討論型態推測「近期可能被搜尋」的話題，並在 trendReason 說明推測理由（不要假裝有外部熱搜來源）
3. label：任務名稱，6～18 字，像企劃案標題，不要標點堆疊
4. seedQuery：種子關鍵字／近期熱詞，2～6 個詞用頓號或空格分隔，適合當 Threads 搜尋起點
5. brief：這次內容計畫摘要，2～4 句，說明話題、受眾痛點、為何適合這個帳號
6. trendReason：1～2 句，為何選這個趨勢（說明它和受眾討論、搜尋意圖或痛點的關係，不要貼 URL）
7. trendKeywords：3～6 個相關熱詞（字串陣列），必須像 Threads 會搜尋/討論的關鍵字，不要像新聞標題
8. angles：3～5 個可勾選切角（字串陣列），每個切角都要說明「這篇要怎麼切」，不要直接寫成完整貼文
9. mission：內容任務，只能是「日常陪伴」「專業知識」「情緒共鳴」「工具清單」「熱門觀點」「互動提問」之一，除非使用者方向要求更精準
10. targetAudience：這篇真正要對誰說，越具體越好
11. openingType：開頭策略，例如「問題開場」「場景開場」「反直覺開場」「經驗開場」「清單開場」
12. bodyType：中段結構，例如「先共鳴再整理」「問題拆解」「步驟清單」「案例對照」「觀點辯證」
13. emotion：情緒槓桿，例如「焦慮被接住」「想被理解」「怕踩雷」「想準備好」「想被鼓勵」
14. ctaType：互動/收藏/私訊等 CTA 型態，不要硬業配
15. riskLevel：low / medium / high；醫療、財務、法律、政策、名人爭議等至少 medium
16. avoid：3～6 個禁忌或不要寫偏的方向（字串陣列）
17. 避開【近期已做過的任務】相同題材
18. 起號優先：題目要能連續延伸 5～10 篇，不要只是一則熱搜心得
19. 除非使用者關鍵字、人設或趨勢訊號明確要求，不要產出早餐、早安、日常飲食、純生活流水帳這類泛題
20. 人設介紹只用來判斷語氣與受眾，不是題材庫。不可因人設寫 Y2K、咖啡、穿搭、音樂，就把靈感主題漂移成咖啡、穿搭或復古日常
21. 若使用者有指定關鍵字，label、seedQuery、brief 都必須保留該方向，不可漂移成無關日常題
22. 繁體中文；只回傳 JSON：
23. 避免新聞導向：不要產出「某事件最新進展」「政策新聞懶人包」「名人/品牌新聞」這類題目；除非它已經轉化成受眾正在問的生活問題或經驗討論
{"label":"","seedQuery":"","brief":"","trendReason":"","trendKeywords":[],"angles":[],"mission":"","targetAudience":"","openingType":"","bodyType":"","emotion":"","ctaType":"","riskLevel":"low","avoid":[]}`)
}

func BuildMissionInspireUserPrompt(in MissionInspireInput) string {
	var b strings.Builder
	if name := strings.TrimSpace(in.PersonaDisplayName); name != "" {
		b.WriteString("【人設名稱】")
		b.WriteString(name)
		b.WriteString("\n")
	}
	if p := strings.TrimSpace(in.PersonaBlock); p != "" {
		b.WriteString("【人設摘要（只用來校準語氣與受眾，不是題材庫）】\n")
		b.WriteString(p)
		b.WriteString("\n注意：不要把人設裡的名詞、興趣或裝飾符號直接變成話題。\n")
	}
	if bench := strings.TrimSpace(in.StyleBenchmark); bench != "" {
		b.WriteString("【對標帳號】@")
		b.WriteString(strings.TrimPrefix(bench, "@"))
		b.WriteString("\n")
	}
	if aud := strings.TrimSpace(in.PersonaAudience); aud != "" {
		b.WriteString("【受眾方向】")
		b.WriteString(aud)
		b.WriteString("\n")
	}
	if goal := strings.TrimSpace(in.PersonaContentGoal); goal != "" {
		b.WriteString("【內容目標】")
		b.WriteString(goal)
		b.WriteString("\n")
	}
	if len(in.PersonaPillars) > 0 {
		b.WriteString("【內容支柱】")
		b.WriteString(strings.Join(in.PersonaPillars, "、"))
		b.WriteString("\n")
	}
	if keyword := strings.TrimSpace(in.UserKeyword); keyword != "" {
		b.WriteString("【使用者指定靈感關鍵字】")
		b.WriteString(keyword)
		b.WriteString("\n")
	}
	if direction := strings.TrimSpace(in.ContentDirection); direction != "" {
		b.WriteString("【使用者選擇內容方向】")
		b.WriteString(direction)
		b.WriteString("\n請讓 label、brief、angles 都符合這個方向；如果方向是 auto，請依熱度與人設自動選。\n")
	}
	if len(in.AvoidTopics) > 0 || len(in.RecentMissionLabels) > 0 || len(in.RecentSeedQueries) > 0 {
		b.WriteString("【近期已做過的任務（請避開）】\n")
		for _, topic := range in.AvoidTopics {
			if topic = strings.TrimSpace(topic); topic != "" {
				b.WriteString("- ")
				b.WriteString(topic)
				b.WriteString("\n")
			}
		}
		for _, label := range in.RecentMissionLabels {
			if label = strings.TrimSpace(label); label != "" {
				b.WriteString("- ")
				b.WriteString(label)
				b.WriteString("\n")
			}
		}
		for _, seed := range in.RecentSeedQueries {
			if seed = strings.TrimSpace(seed); seed != "" {
				b.WriteString("- 種子：")
				b.WriteString(seed)
				b.WriteString("\n")
			}
		}
	}
	if in.LLMOnly {
		b.WriteString("【模式】未取得 Threads/API/搜尋素材，請純依人設、使用者關鍵字與受眾推測靈感（勿假裝有外部熱搜）\n")
	} else if provider := strings.TrimSpace(in.WebSearchProvider); provider != "" {
		b.WriteString("【趨勢來源】")
		b.WriteString(provider)
		b.WriteString("\n")
	}
	b.WriteString("【近期趨勢訊號】\n")
	if len(in.TrendSnippets) == 0 {
		b.WriteString("（無外部趨勢結果，請改用人設推測近期 Threads 可能熱議方向）\n")
	} else {
		for i, item := range in.TrendSnippets {
			b.WriteString(fmt.Sprintf("[%d] 查詢：%s\n", i+1, strings.TrimSpace(item.Query)))
			if title := strings.TrimSpace(item.Title); title != "" {
				b.WriteString("標題：")
				b.WriteString(title)
				b.WriteString("\n")
			}
			if snippet := strings.TrimSpace(item.Snippet); snippet != "" {
				b.WriteString(snippet)
				b.WriteString("\n")
			}
			b.WriteString("\n")
		}
	}
	b.WriteString("請產出 Topic Radar 與 Content Plan JSON。")
	return b.String()
}

func InspireTrendSearchQueries(personaBrief, styleBenchmark string) []string {
	queries := []string{
		"Threads 台灣 熱門 關鍵字 討論 最近 一週",
		"Threads 台灣 請問 推薦 心得 最近",
		"社群 熱門 關鍵字 台灣 討論 Dcard PTT Threads",
	}
	context := strings.TrimSpace(personaBrief + " " + styleBenchmark)
	if context != "" {
		trimmed := context
		if len([]rune(trimmed)) > 24 {
			trimmed = string([]rune(trimmed)[:24])
		}
		queries = append(queries,
			trimmed+" Threads 熱門 關鍵字 討論",
			trimmed+" 請問 心得 推薦 Threads",
		)
	}
	return queries
}

func InspireThreadsSearchQueries(in MissionInspireThreadsInput) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, 6)
	add := func(q string) {
		q = cleanInspireQuery(q)
		if q == "" {
			return
		}
		if _, ok := seen[q]; ok {
			return
		}
		seen[q] = struct{}{}
		out = append(out, q)
	}

	keyword := cleanInspireQuery(in.Keyword)
	if keyword != "" {
		add(keyword)
		add(keyword + " 熱門")
		add(keyword + " 心得")
		add(keyword + " 請問")
		add(keyword + " 推薦")
		add(keyword + " 怎麼辦")
	}
	for _, pillar := range in.Pillars {
		if len(out) >= 6 {
			break
		}
		add(pillar)
		if keyword != "" {
			add(keyword + " " + pillar)
		}
	}
	for _, question := range in.Questions {
		if len(out) >= 6 {
			break
		}
		add(question)
	}
	if len(out) == 0 {
		context := strings.TrimSpace(in.PersonaBrief + " " + in.StyleBenchmark)
		if len([]rune(context)) > 24 {
			context = string([]rune(context)[:24])
		}
		add(context)
	}
	if len(out) > 6 {
		return out[:6]
	}
	return out
}

func CollectMissionInspireThreadsTrends(ctx context.Context, in MissionInspireThreadsInput) []MissionInspireTrendSnippet {
	if !in.Member.HasDiscoverPath() {
		return nil
	}
	if !in.Member.AllowsCrawler && !in.Member.AllowsThreadsAPI {
		return nil
	}
	queries := InspireThreadsSearchQueries(in)
	if len(queries) == 0 {
		return nil
	}

	type scoredSnippet struct {
		item  MissionInspireTrendSnippet
		score int
	}
	seen := map[string]struct{}{}
	collected := make([]scoredSnippet, 0, 12)
	for _, query := range queries {
		posts, _, err := placement.Discover(ctx, placement.DiscoverRequest{
			Query:   query,
			Keyword: query,
			Limit:   8,
			Member:  in.Member,
			Crawler: in.Crawler,
		})
		if err != nil {
			continue
		}
		for _, post := range posts {
			text := strings.TrimSpace(post.Text)
			if len([]rune(text)) < 18 {
				continue
			}
			key := strings.TrimSpace(post.Permalink)
			if key == "" {
				key = strings.TrimSpace(post.ExternalID)
			}
			if key == "" {
				key = text
			}
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
			title := strings.TrimSpace(post.Author)
			if title != "" {
				title = "Threads @" + strings.TrimPrefix(title, "@")
			} else {
				title = "Threads 熱門貼文"
			}
			collected = append(collected, scoredSnippet{
				item: MissionInspireTrendSnippet{
					Query:   query,
					Title:   title,
					Snippet: shortenRunes(text, 180),
					URL:     strings.TrimSpace(post.Permalink),
				},
				score: post.LikeCount + post.ReplyCount*2,
			})
		}
		if len(collected) >= 12 {
			break
		}
	}
	sort.SliceStable(collected, func(i, j int) bool {
		return collected[i].score > collected[j].score
	})
	if len(collected) > 8 {
		collected = collected[:8]
	}
	out := make([]MissionInspireTrendSnippet, 0, len(collected))
	for _, item := range collected {
		out = append(out, item.item)
	}
	return out
}

func cleanInspireQuery(raw string) string {
	raw = strings.TrimSpace(raw)
	raw = strings.ReplaceAll(raw, "\n", " ")
	raw = strings.Join(strings.Fields(raw), " ")
	if len([]rune(raw)) > 24 {
		raw = string([]rune(raw)[:24])
	}
	return raw
}

func shortenRunes(raw string, max int) string {
	raw = strings.TrimSpace(raw)
	if max <= 0 || len([]rune(raw)) <= max {
		return raw
	}
	return string([]rune(raw)[:max]) + "…"
}

func ParseMissionInspireOutput(raw string) (MissionInspireOutput, error) {
	payload, err := extractCopyMapJSON(raw)
	if err != nil {
		return MissionInspireOutput{}, err
	}
	var root map[string]json.RawMessage
	if err := json.Unmarshal(payload, &root); err != nil {
		return MissionInspireOutput{}, err
	}
	out := MissionInspireOutput{
		Label:          firstJSONString(root, "label", "title", "mission_label"),
		SeedQuery:      firstJSONString(root, "seedQuery", "seed_query", "seed", "keywords"),
		Brief:          firstJSONString(root, "brief", "description", "goal"),
		TrendReason:    firstJSONString(root, "trendReason", "trend_reason", "reason"),
		Mission:        firstJSONString(root, "mission", "contentMission", "content_mission"),
		TargetAudience: firstJSONString(root, "targetAudience", "target_audience", "audience"),
		OpeningType:    firstJSONString(root, "openingType", "opening_type", "hookType", "hook_type"),
		BodyType:       firstJSONString(root, "bodyType", "body_type", "structure", "bodyStructure"),
		Emotion:        firstJSONString(root, "emotion", "emotionalLever", "emotional_lever"),
		CtaType:        firstJSONString(root, "ctaType", "cta_type", "cta"),
		RiskLevel:      firstJSONString(root, "riskLevel", "risk_level", "risk"),
	}
	if out.Label == "" || out.SeedQuery == "" || out.Brief == "" {
		return MissionInspireOutput{}, fmt.Errorf("missing label, seedQuery, or brief")
	}
	if rawKW, ok := root["trendKeywords"]; ok {
		out.TrendKeywords = parseFlexibleStringList(rawKW)
	}
	if len(out.TrendKeywords) == 0 {
		if rawKW, ok := root["trend_keywords"]; ok {
			out.TrendKeywords = parseFlexibleStringList(rawKW)
		}
	}
	if rawAngles, ok := root["angles"]; ok {
		out.Angles = parseFlexibleStringList(rawAngles)
	}
	if len(out.Angles) == 0 {
		if rawAngles, ok := root["contentAngles"]; ok {
			out.Angles = parseFlexibleStringList(rawAngles)
		}
	}
	if rawAvoid, ok := root["avoid"]; ok {
		out.Avoid = parseFlexibleStringList(rawAvoid)
	}
	if len(out.Avoid) == 0 {
		if rawAvoid, ok := root["avoidTopics"]; ok {
			out.Avoid = parseFlexibleStringList(rawAvoid)
		}
	}
	return out, nil
}
