package usecase

import (
	"crypto/sha256"
	"encoding/hex"
	"net/url"
	"strconv"
	"strings"
	"unicode/utf8"

	"apps/backend/internal/module/scout/domain"
)

type classifiedPost struct {
	classification string
	score          int
	reason         string
}

// planScanTerms builds independent search queries (one query per term).
// Callers fan out: each ScanTerms[i] is searched separately then merged.
// Avoids dumping all keywords into one diluted Exa query.
func planScanTerms(brief *domain.RunBrief) []string {
	if brief == nil {
		return nil
	}
	if brief.Mode == domain.ModeActivity {
		// 規則式變體（不依賴 AI）；AI 擴充在 PrepareBrief 另外合併。
		return planActivityTerms(brief.Intent)
	}
	if brief.Mode == domain.ModeProvider {
		return planProviderTerms(brief.Pains, brief.Tags)
	}
	if brief.Mode == domain.ModeDemand {
		return planDemandTerms(brief.Pains)
	}

	pains := filterSeekablePains(brief.Pains)
	tags := filterSeekablePains(brief.Tags)
	var queries []string

	intent := strings.TrimSpace(brief.Intent)
	if intent != "" {
		if utf8.RuneCountInString(intent) > 48 {
			intent = truncateRunes(intent, 48)
		}
		queries = append(queries, intent)
	}
	for _, p := range pains {
		if !strings.EqualFold(p, intent) {
			queries = append(queries, p)
		}
	}
	for _, tag := range tags {
		if !strings.EqualFold(tag, intent) {
			queries = append(queries, tag)
		}
	}
	// 對前幾個短痛點加求助語感 variant（整句當一 query，不是單獨搜「求推薦」）
	for _, p := range capTerms(pains, 3) {
		if utf8.RuneCountInString(p) <= 18 {
			queries = append(queries, p+" 求推薦")
		}
	}
	return capTerms(dedupeTerms(queries), 8)
}

func planDemandTerms(pains []string) []string {
	pains = compactProviderTerms(pains)
	var queries []string
	for _, pain := range pains {
		queries = append(queries, pain+" 怎麼辦", pain+" 推薦", pain+" 有人也這樣嗎")
	}
	return capTerms(dedupeTerms(queries), 6)
}

func planProviderTerms(pains, capabilities []string) []string {
	pains = compactProviderTerms(pains)
	capabilities = compactProviderTerms(capabilities)
	var queries []string
	for _, pain := range pains {
		for _, capability := range capabilities {
			if strings.EqualFold(pain, capability) {
				queries = append(queries, capability+" 推薦", capability+" 專業")
				continue
			}
			if utf8.RuneCountInString(pain)+utf8.RuneCountInString(capability)+1 <= 24 {
				queries = append(queries, pain+" "+capability)
			}
		}
		queries = append(queries, pain+" 推薦")
	}
	for _, capability := range capabilities {
		queries = append(queries, capability+" 推薦", capability+" 專業")
	}
	return capTerms(dedupeTerms(queries), 8)
}

func compactProviderTerms(terms []string) []string {
	var out []string
	for _, term := range terms {
		term = strings.Join(strings.Fields(strings.TrimSpace(term)), " ")
		for _, filler := range []string{"不知道怎麼辦", "怎麼辦", "有沒有推薦", "求推薦", "請問", "想問", "我想找", "需要"} {
			term = strings.ReplaceAll(term, filler, "")
		}
		term = strings.Trim(term, " ,，、。！？!?：:")
		if utf8.RuneCountInString(term) > 18 {
			term = truncateRunes(term, 18)
		}
		if term != "" {
			out = append(out, term)
		}
	}
	return filterSeekablePains(out)
}

// filterSeekablePains drops marketing slogans / long product blurbs that pull competitor posts.
func filterSeekablePains(in []string) []string {
	var out []string
	for _, raw := range in {
		term := strings.Join(strings.Fields(strings.TrimSpace(raw)), " ")
		if term == "" {
			continue
		}
		if isMarketingPhrase(term) {
			continue
		}
		out = append(out, term)
	}
	return out
}

func isMarketingPhrase(term string) bool {
	if utf8.RuneCountInString(term) > 28 {
		return true
	}
	lower := strings.ToLower(term)
	bad := []string{
		"✔", "✓", "x1", "×1", "任選", "內含", "官網", "現折", "折扣碼",
		"完整保養流程", "快速帶走", "滋潤修護", "天然植萃配方", "溫和不刺激",
		"服務洽詢", "限時優惠", "立即購買", "旗艦店",
	}
	for _, b := range bad {
		if strings.Contains(lower, strings.ToLower(b)) {
			return true
		}
	}
	return false
}

func capTerms(terms []string, max int) []string {
	if max <= 0 || len(terms) <= max {
		return terms
	}
	return terms[:max]
}

func dedupeTerms(groups ...[]string) []string {
	seen := make(map[string]struct{})
	var out []string
	for _, group := range groups {
		for _, term := range group {
			term = strings.Join(strings.Fields(strings.TrimSpace(term)), " ")
			key := strings.ToLower(term)
			if term == "" {
				continue
			}
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
			out = append(out, term)
		}
	}
	return out
}

func tokenizeIntent(intent string) []string {
	// 簡單空白／標點切詞，活躍模式備援
	fields := strings.FieldsFunc(intent, func(r rune) bool {
		switch r {
		case ' ', '\t', '\n', ',', '，', '、', '/', '|', '·', '。', '！', '？', '!', '?':
			return true
		default:
			return false
		}
	})
	return fields
}

func truncateRunes(s string, max int) string {
	if max <= 0 || utf8.RuneCountInString(s) <= max {
		return s
	}
	runes := []rune(s)
	return string(runes[:max])
}

func classifyPost(mode, text string, terms []string) classifiedPost {
	lower := strings.ToLower(text)
	signals := matchedSignals(lower, terms)
	if hasAny(lower, "giveaway", "抽獎", "follow for follow", "互追", "crypto", "賺錢") {
		return classifiedPost{domain.ClassificationNoise, 0, "noise signal"}
	}
	if mode == domain.ModeActivity {
		return classifyActivity(lower, signals)
	}
	if hasAny(lower, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") {
		return scored(domain.ClassificationProviderOffer, 35, signals, "provider-offer signal")
	}
	if hasAny(lower, "推薦", "求推", "有沒有推薦", "any recommendation", "what do you recommend") {
		return scored(domain.ClassificationSeekingRecommendation, 60, signals, "recommendation signal")
	}
	if strings.Contains(lower, "?") || strings.Contains(lower, "？") || hasAny(lower, "怎麼", "如何", "請問", "求助", "help") {
		return scored(domain.ClassificationSeekingHelp, 55, signals, "help-seeking signal")
	}
	return scored(domain.ClassificationDiscussion, 40, signals, "discussion signal")
}

func classifyProvider(text string, pains, capabilities, excludes []string) classifiedPost {
	lower := strings.ToLower(text)
	if hasMatchingTerm(lower, excludes) {
		return classifiedPost{domain.ClassificationNoise, 0, "same-category exclusion"}
	}
	if hasAny(lower, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") {
		return classifiedPost{domain.ClassificationNoise, 0, "sales signal"}
	}
	matchedPains := matchedSignals(lower, pains)
	matchedCapabilities := matchedSignals(lower, capabilities)
	if len(matchedPains) == 0 || len(matchedCapabilities) == 0 {
		return classifiedPost{domain.ClassificationNoise, 0, "missing pain or capability evidence"}
	}
	base := 65
	classification := domain.ClassificationProviderDirect
	reason := "pain and capability evidence"
	if hasAny(lower, "推薦", "介紹", "找", "口碑") {
		base = 75
		classification = domain.ClassificationProviderRecommended
		reason = "recommended provider evidence"
	} else if !hasAny(lower, "案例", "專業", "預約", "諮詢", "服務", "協助", "聯絡", "工作室", "診所", "顧問") {
		return classifiedPost{domain.ClassificationNoise, 0, "missing provider proof"}
	}
	signals := append(matchedPains, matchedCapabilities...)
	return scored(classification, base, signals, reason)
}

func classifyDemand(text string, pains, excludes []string) classifiedPost {
	lower := strings.ToLower(text)
	if hasMatchingTerm(lower, excludes) || hasAny(lower, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") {
		return classifiedPost{domain.ClassificationNoise, 0, "excluded sales or category signal"}
	}
	signals := matchedSignals(lower, pains)
	if len(signals) == 0 {
		return classifiedPost{domain.ClassificationNoise, 0, "missing pain evidence"}
	}
	if hasAny(lower, "推薦", "求推", "有沒有推薦", "有人也", "哪裡", "找不到") {
		return scored(domain.ClassificationSeekingRecommendation, 75, signals, "pain and recommendation signal")
	}
	if strings.Contains(lower, "?") || strings.Contains(lower, "？") || hasAny(lower, "怎麼", "如何", "請問", "求助", "help", "沒用", "無效", "失敗", "困擾") {
		return scored(domain.ClassificationSeekingHelp, 70, signals, "pain and help signal")
	}
	return classifiedPost{domain.ClassificationNoise, 0, "pain mentioned without demand signal"}
}

func hasMatchingTerm(text string, terms []string) bool {
	return len(matchedSignals(text, terms)) > 0
}

func classifyActivity(text string, signals []string) classifiedPost {
	if hasAny(text, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") {
		return scored(domain.ClassificationProviderOffer, 35, signals, "provider-offer signal; recency unavailable (neutral)")
	}
	if strings.Contains(text, "?") || strings.Contains(text, "？") || hasAny(text, "請問", "怎麼", "如何", "有人知道", "求") {
		return scored(domain.ClassificationAsking, 60, signals, "asking signal; recency unavailable (neutral)")
	}
	if hasAny(text, "活動", "event", "開幕", "launch", "發布", "報名", "登記", "公告") {
		return scored(domain.ClassificationAnnouncement, 50, signals, "announcement signal; recency unavailable (neutral)")
	}
	return scored(domain.ClassificationDiscussion, 40, signals, "discussion signal; recency unavailable (neutral)")
}

func scored(classification string, base int, signals []string, label string) classifiedPost {
	score := base + len(signals)*15
	if score > 100 {
		score = 100
	}
	reason := label
	if len(signals) > 0 {
		reason += "; matched: " + strings.Join(signals, ", ")
	}
	return classifiedPost{classification, score, reason}
}

func matchedSignals(text string, terms []string) []string {
	var signals []string
	text = normalizeSignal(text)
	for _, term := range dedupeTerms(terms) {
		if normalized := normalizeSignal(term); normalized != "" && strings.Contains(text, normalized) {
			signals = append(signals, term)
		}
	}
	return signals
}

func normalizeSignal(text string) string {
	text = strings.ToLower(text)
	for _, filler := range []string{"一直", "真的", "有點", "又", "很", "都"} {
		text = strings.ReplaceAll(text, filler, "")
	}
	return strings.Join(strings.Fields(text), "")
}

func hasAny(text string, signals ...string) bool {
	for _, signal := range signals {
		if strings.Contains(text, signal) {
			return true
		}
	}
	return false
}

func isThreadsHostname(host string) bool {
	host = strings.ToLower(strings.TrimSpace(host))
	return host == "threads.net" || strings.HasSuffix(host, ".threads.net") ||
		host == "threads.com" || strings.HasSuffix(host, ".threads.com")
}

// threadsPostShortcode extracts the stable post identity from both profile
// permalinks (/@user/post/CODE) and short share links (/t/CODE).
func threadsPostShortcode(raw string) string {
	u, err := url.Parse(strings.TrimSpace(raw))
	if err != nil || !isThreadsHostname(u.Hostname()) {
		return ""
	}
	parts := strings.Split(strings.Trim(u.Path, "/"), "/")
	for i := 0; i+1 < len(parts); i++ {
		segment := strings.ToLower(strings.TrimSpace(parts[i]))
		if segment != "post" && segment != "t" {
			continue
		}
		if code := strings.TrimSpace(parts[i+1]); code != "" {
			return code
		}
	}
	return ""
}

// canonicalPostIdentity is the dedupe key. The shortcode stays case-sensitive;
// threads.net/.com, www/mobile hosts, query strings and author path changes do not.
func canonicalPostIdentity(raw string) string {
	if code := threadsPostShortcode(raw); code != "" {
		return "threads-post:" + code
	}
	if permalink := canonicalPermalink(raw); permalink != "" {
		return "url:" + permalink
	}
	return ""
}

func canonicalPermalink(raw string) string {
	u, err := url.Parse(strings.TrimSpace(raw))
	if err != nil || u.Host == "" {
		return ""
	}
	u.Scheme = "https"
	if isThreadsHostname(u.Hostname()) {
		u.Host = "www.threads.net"
	} else {
		u.Host = strings.ToLower(u.Host)
	}
	u.User = nil
	u.RawQuery = ""
	u.Fragment = ""
	u.Path = strings.TrimRight(u.Path, "/")
	u.RawPath = ""
	return u.String()
}

func permalinkID(ownerUID int64, permalink string) string {
	identity := canonicalPostIdentity(permalink)
	if identity == "" {
		identity = strings.TrimSpace(permalink)
	}
	sum := sha256.Sum256([]byte(identity + "|" + formatOwnerUID(ownerUID)))
	return "sp_" + hex.EncodeToString(sum[:])[:20]
}

func formatOwnerUID(ownerUID int64) string {
	return strconv.FormatInt(ownerUID, 10)
}
