package domain

import (
	"strings"
	"unicode"
	"unicode/utf8"
)

// Threads 搜尋短詞硬約束（中文斷詞差、長字串常查無結果）。
// 一組查詢 = 最多 2 個 token（半形空格分隔）；中文 token 2–4 字；整組去掉空格後 ≤12 字元。
const (
	MaxThreadsTokens    = 2
	MinCJKTokenRunes    = 2
	MaxCJKTokenRunes    = 4
	MaxThreadsTermRunes = 12 // 去掉空白後
	MaxExploreTerms     = 6
	// 一則長句最多收成幾組可搜短詞，避免滑窗把訂閱或需求地圖塞滿半截字。
	MaxSearchableVariantsPerTerm = 3
	MaxSearchableVariants        = 8
)

// NormalizeSearchTerm trims, converts full-width spaces to half-width, and collapses whitespace.
func NormalizeSearchTerm(raw string) string {
	s := strings.ReplaceAll(raw, "\u3000", " ")
	return strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
}

// IsThreadsSearchable reports whether a (already-normalized or raw) term meets Threads short-term rules.
//
// Rules:
//   - after normalize: non-empty
//   - ≤2 tokens (space-separated)
//   - CJK tokens: 2–4 runes; Latin tokens: 2–12 runes (single token)
//   - total runes without spaces ≤12
//   - no punctuation, quotes, boolean operators, emoji, or #
func IsThreadsSearchable(term string) bool {
	term = NormalizeSearchTerm(term)
	if term == "" {
		return false
	}
	// total runes without spaces
	compact := strings.ReplaceAll(term, " ", "")
	if utf8.RuneCountInString(compact) > MaxThreadsTermRunes {
		return false
	}
	tokens := strings.Fields(term)
	if len(tokens) == 0 || len(tokens) > MaxThreadsTokens {
		return false
	}
	for _, tok := range tokens {
		if !isAllowedToken(tok) {
			return false
		}
	}
	return true
}

func isAllowedToken(tok string) bool {
	if tok == "" {
		return false
	}
	// reject common boolean / operators whole-token
	upper := strings.ToUpper(tok)
	switch upper {
	case "AND", "OR", "NOT":
		return false
	}
	hasCJK := false
	hasLetter := false
	for _, r := range tok {
		if r == '#' || r == '"' || r == '\'' || r == '「' || r == '」' || r == '『' || r == '』' {
			return false
		}
		if r == '-' || r == '+' || r == '*' || r == '(' || r == ')' || r == '|' || r == '&' {
			return false
		}
		if unicode.IsPunct(r) || unicode.IsSymbol(r) {
			return false
		}
		// emoji / other symbols often in Symbol or So; also catch common ranges
		if r >= 0x1F300 && r <= 0x1FAFF {
			return false
		}
		if r >= 0x2600 && r <= 0x27BF {
			return false
		}
		if isCJK(r) {
			hasCJK = true
			continue
		}
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
			hasLetter = true
			continue
		}
		// allow middle dot / common CJK connector? no — keep strict
		return false
	}
	n := utf8.RuneCountInString(tok)
	if hasCJK {
		// mixed CJK+latin in one token: count as CJK rule by total length
		return n >= MinCJKTokenRunes && n <= MaxCJKTokenRunes
	}
	if hasLetter {
		return n >= 2 && n <= MaxThreadsTermRunes
	}
	return false
}

func isCJK(r rune) bool {
	return unicode.Is(unicode.Han, r) ||
		unicode.Is(unicode.Hiragana, r) ||
		unicode.Is(unicode.Katakana, r) ||
		(r >= 0x3000 && r <= 0x303F) // CJK punctuation block — treated as CJK char class but punct rejected above
}

var searchFillers = []string{
	"怎麼辦", "求推薦", "有沒有人", "有人知道", "請問一下", "請問",
	"想問", "想找", "有沒有", "可以嗎", "好不好",
}

// SearchableTermVariants turns a user/product phrase into Threads-searchable queries.
// If the input already passes IsThreadsSearchable it is the only result.
// Longer phrases are shortened: adjacent token pairs, filler tails stripped,
// then first/last 2–4 CJK windows. include=false keeps the broader exclude length.
func SearchableTermVariants(raw string, include bool) []string {
	raw = NormalizeSearchTerm(raw)
	if raw == "" {
		return nil
	}
	seen := map[string]bool{}
	out := make([]string, 0, MaxSearchableVariants)
	add := func(term string) {
		term = NormalizeSearchTerm(term)
		if term == "" || seen[strings.ToLower(term)] {
			return
		}
		if include {
			if !IsThreadsSearchable(term) {
				return
			}
		} else if n := utf8.RuneCountInString(term); n < MinTermLen || n > MaxTermLen {
			return
		}
		seen[strings.ToLower(term)] = true
		out = append(out, term)
	}

	if include && IsThreadsSearchable(raw) {
		add(raw)
		return out
	}
	if !include {
		add(raw)
		if len(out) > 0 {
			return out
		}
	}

	tokens := strings.Fields(raw)
	if len(tokens) > MaxThreadsTokens {
		for i := 0; i+1 < len(tokens) && len(out) < MaxSearchableVariants; i++ {
			add(tokens[i] + " " + tokens[i+1])
		}
		for _, tok := range tokens {
			if len(out) >= MaxSearchableVariants {
				break
			}
			for _, v := range SearchableTermVariants(tok, include) {
				add(v)
				if len(out) >= MaxSearchableVariants {
					break
				}
			}
		}
		return out
	}

	parts := splitSearchParts(raw)
	if len(parts) == 0 {
		parts = []string{raw}
	}
	for _, part := range parts {
		if len(out) >= MaxSearchableVariants {
			break
		}
		if include && IsThreadsSearchable(part) {
			add(part)
			continue
		}
		stripped := stripSearchFillers(part)
		if stripped != "" && stripped != part {
			add(stripped)
		}
		runes := []rune(stripped)
		if len(runes) == 0 {
			runes = []rune(part)
		}
		if len(runes) == 5 {
			add(string(runes[:3]))
			add(string(runes[3:]))
		}
		for _, width := range []int{4, 3, 2} {
			if len(runes) < width {
				continue
			}
			add(string(runes[:width]))
			if len(runes) > width {
				add(string(runes[len(runes)-width:]))
			}
		}
		if len(out) >= 3 {
			continue
		}
		for width := 4; width >= 2; width-- {
			if len(runes) < width {
				continue
			}
			for start := 0; start+width <= len(runes) && len(out) < MaxSearchableVariants; start++ {
				add(string(runes[start : start+width]))
			}
		}
	}
	return out
}

// ExpandSearchTerms keeps already-searchable include terms and shortens the rest.
// Used when persisting a watch and when fanning out a sweep.
func ExpandSearchTerms(terms []string, max int) []string {
	if max <= 0 {
		max = MaxWatchTerms
	}
	out := make([]string, 0, max)
	seen := map[string]bool{}
	add := func(term string) bool {
		term = strings.ToLower(NormalizeSearchTerm(term))
		if term == "" || !IsThreadsSearchable(term) || seen[term] {
			return len(out) < max
		}
		seen[term] = true
		out = append(out, term)
		return len(out) < max
	}
	for _, raw := range terms {
		raw = NormalizeSearchTerm(raw)
		if raw == "" {
			continue
		}
		if IsThreadsSearchable(raw) {
			if !add(raw) {
				return out
			}
			continue
		}
		for i, v := range SearchableTermVariants(raw, true) {
			if i >= MaxSearchableVariantsPerTerm {
				break
			}
			if !add(v) {
				return out
			}
		}
	}
	return out
}

func stripSearchFillers(s string) string {
	for _, filler := range searchFillers {
		s = strings.ReplaceAll(s, filler, "")
	}
	return strings.Trim(s, " ,，、。！？!?：:的了嗎呢啊喔唷")
}

func splitSearchParts(raw string) []string {
	var b strings.Builder
	parts := make([]string, 0, 4)
	flush := func() {
		if value := strings.TrimSpace(b.String()); value != "" {
			parts = append(parts, value)
		}
		b.Reset()
	}
	for _, r := range raw {
		if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.Is(unicode.Han, r) || unicode.Is(unicode.Hiragana, r) || unicode.Is(unicode.Katakana, r) {
			b.WriteRune(r)
		} else {
			flush()
		}
	}
	flush()
	return parts
}
