package knowledge

import (
	"strings"
	"unicode"
)

// IntentProfile is a weighted lexical intent model for product + research map.
// It generalizes relevance beyond hardcoded category rules (e.g. 洗衣精 vs 洗衣機).
type IntentProfile struct {
	TokenWeights map[string]float64
	BroadTokens  map[string]struct{}
	Phrases      []weightedPhrase
}

type weightedPhrase struct {
	Text   string
	Weight float64
}

const (
	intentWeightProduct  = 3.0
	intentWeightFeature  = 2.8
	intentWeightMatchTag = 2.5
	intentWeightQuestion = 2.2
	intentWeightPillar   = 2.0
	intentWeightAudience = 1.6
	intentWeightPatrol   = 1.2
	intentWeightSeed     = 1.0

	tangentialBroadMin  = 38
	tangentialIntentMax = 30
)

var productFormHints = []string{
	"洗衣精", "洗衣粉", "洗衣劑", "洗衣液",
	"沐浴乳", "沐浴露", "洗髮精", "洗髮乳", "洗面乳",
}

// BuildIntentProfile materializes product + research-map text into a reusable relevance model.
func BuildIntentProfile(in PatrolTagInput) IntentProfile {
	profile := IntentProfile{
		TokenWeights: map[string]float64{},
		BroadTokens:  map[string]struct{}{},
	}
	addPhrase := func(text string, weight float64) {
		text = strings.TrimSpace(text)
		if text == "" || weight <= 0 {
			return
		}
		profile.Phrases = append(profile.Phrases, weightedPhrase{Text: text, Weight: weight})
		profile.addTokens(text, weight)
	}

	addPhrase(in.ProductName, intentWeightProduct)
	addPhrase(in.ProductFeatures, intentWeightFeature)
	for _, tag := range in.MatchTags {
		addPhrase(tag, intentWeightMatchTag)
	}
	for _, q := range in.Questions {
		addPhrase(q, intentWeightQuestion)
	}
	for _, pillar := range in.Pillars {
		addPhrase(pillar, intentWeightPillar)
	}
	addPhrase(in.AudienceSummary, intentWeightAudience)
	addPhrase(in.TargetAudience, intentWeightAudience)
	for _, kw := range in.PatrolKeywords {
		addPhrase(kw, intentWeightPatrol)
	}

	if hint := productCategoryHint(in.ProductName, in.ProductFeatures); hint != "" {
		profile.markBroad(hint)
	}
	for _, hint := range productFormHints {
		blob := strings.ToLower(in.ProductName + " " + in.ProductFeatures + " " + strings.Join(in.MatchTags, " "))
		if strings.Contains(blob, hint) {
			profile.markBroad(hint)
		}
	}
	return profile
}

func (p *IntentProfile) addTokens(text string, weight float64) {
	for _, token := range intentTokenize(text) {
		if existing, ok := p.TokenWeights[token]; !ok || weight > existing {
			p.TokenWeights[token] = weight
		}
	}
}

func (p *IntentProfile) markBroad(token string) {
	token = strings.TrimSpace(strings.ToLower(token))
	if token == "" {
		return
	}
	p.BroadTokens[token] = struct{}{}
	for _, part := range intentTokenize(token) {
		p.BroadTokens[part] = struct{}{}
	}
}

func (p IntentProfile) totalWeight() float64 {
	sum := 0.0
	for _, w := range p.TokenWeights {
		sum += w
	}
	if sum <= 0 {
		for _, phrase := range p.Phrases {
			sum += phrase.Weight
		}
	}
	return sum
}

// NodeIntentText combines node fields used for semantic fit.
func NodeIntentText(node Node) string {
	return strings.TrimSpace(strings.Join([]string{
		node.Label,
		node.Relation,
		node.PlacementValue,
		strings.Join(node.PatrolRelevance, " "),
		strings.Join(node.PatrolRecency, " "),
	}, " "))
}

// ScoreIntentSimilarity returns 0-100 weighted token overlap against the intent profile.
func ScoreIntentSimilarity(text string, profile IntentProfile) int {
	text = strings.ToLower(strings.TrimSpace(text))
	if text == "" || len(profile.TokenWeights) == 0 {
		return 0
	}
	matched := 0.0
	for token, weight := range profile.TokenWeights {
		if strings.Contains(text, token) {
			matched += weight
		}
	}
	total := profile.totalWeight()
	if total <= 0 {
		return 0
	}
	score := int((matched / total) * 100)
	if score > 100 {
		return 100
	}
	return score
}

// ScoreBroadSimilarity measures overlap with category-level tokens only.
func ScoreBroadSimilarity(text string, profile IntentProfile) int {
	text = strings.ToLower(strings.TrimSpace(text))
	if text == "" || len(profile.BroadTokens) == 0 {
		return 0
	}
	hits := 0
	for token := range profile.BroadTokens {
		if strings.Contains(text, token) {
			hits++
			continue
		}
		runes := []rune(token)
		if len(runes) >= 2 && strings.Contains(text, string(runes[:2])) {
			hits++
		}
	}
	if hits == 0 {
		return 0
	}
	denom := len(profile.BroadTokens)
	if denom > 6 {
		denom = 6
	}
	score := (hits * 100) / denom
	if score > 100 {
		return 100
	}
	return score
}

// IsTangentialToIntent detects same broad category but weak product-intent alignment.
// Example: 洗衣機 vs 抗敏無香洗衣精 — shares 洗衣, lacks 無香/敏感/化療 intent.
func IsTangentialToIntent(text string, profile IntentProfile) bool {
	text = strings.ToLower(strings.TrimSpace(text))
	if text == "" || len(profile.TokenWeights) == 0 {
		return false
	}
	intentScore := ScoreIntentSimilarity(text, profile)
	if intentScore >= tangentialIntentMax+10 {
		return false
	}
	if hasCategoryStemDrift(text, profile) {
		return true
	}
	broadScore := ScoreBroadSimilarity(text, profile)
	if broadScore >= tangentialBroadMin {
		return intentScore <= tangentialIntentMax
	}
	return false
}

func hasCategoryStemDrift(text string, profile IntentProfile) bool {
	for broad := range profile.BroadTokens {
		runes := []rune(broad)
		if len(runes) < 2 {
			continue
		}
		stem := string(runes[:2])
		if !strings.Contains(text, stem) {
			continue
		}
		if strings.Contains(text, broad) {
			continue
		}
		for _, suffix := range []string{"機", "槽", "烘", "劑", "粉", "精", "乳", "露", "液", "皂"} {
			alt := stem + suffix
			if alt == broad || !strings.Contains(text, alt) {
				continue
			}
			if !profile.matchesIntentToken(alt) {
				return true
			}
		}
	}
	return false
}

func (p IntentProfile) matchesIntentToken(token string) bool {
	if _, ok := p.TokenWeights[token]; ok {
		return true
	}
	for intentToken := range p.TokenWeights {
		if strings.Contains(token, intentToken) || strings.Contains(intentToken, token) {
			return true
		}
	}
	return false
}

func intentTokenize(text string) []string {
	text = strings.ToLower(strings.TrimSpace(text))
	if text == "" {
		return nil
	}
	repl := strings.NewReplacer("，", " ", "、", " ", "。", " ", "；", " ", "/", " ", "｜", " ", "|", " ", "（", " ", "）", " ", "(", " ", ")", " ", "？", " ", "?", " ", "！", " ", "!", " ")
	text = repl.Replace(text)
	seen := map[string]struct{}{}
	var out []string
	add := func(token string) {
		token = strings.Trim(token, `"'「」『』：:`)
		if len([]rune(token)) < 2 {
			return
		}
		if _, ok := seen[token]; ok {
			return
		}
		seen[token] = struct{}{}
		out = append(out, token)
	}
	for _, part := range strings.Fields(text) {
		add(part)
	}
	if len(out) == 0 {
		for _, chunk := range intentSplitRunes(text) {
			add(chunk)
		}
	}
	return out
}

func intentSplitRunes(s string) []string {
	var out []string
	var buf []rune
	flush := func() {
		if len(buf) >= 2 {
			out = append(out, string(buf))
		}
		buf = buf[:0]
	}
	for _, r := range s {
		if unicode.Is(unicode.Han, r) {
			buf = append(buf, r)
			continue
		}
		flush()
	}
	flush()
	return out
}
