// Package analyzer provides keyword extraction and language detection.
// analyzer 包提供文本分词、关键词提取和语种检测功能。
//
// 分词策略：中文使用 gojieba（结巴分词 C++）进行精确/搜索模式分词；
// 纯 ASCII 英文按空格切分。语言检测使用 lingua-go（纯 Go，无外部模型文件）。
package analyzer

import (
	"encoding/json" // JSON 反序列化（加载屏蔽词列表）
	"math"          // 数学运算（最小值、开方）
	"os"            // 文件系统操作（读取屏蔽词文件）
	"path/filepath" // 路径处理
	"strings"       // 字符串操作
	"sync"          // 互斥锁（保护 jieba 的非线程安全调用）
	"unicode"       // Unicode 字符判断

	"github.com/pemistahl/lingua-go" // 纯 Go 语言检测库（支持 75 种语言）
	"github.com/yanyiwu/gojieba"     // Gojieba：C++ 结巴分词的 Go 封装
)

// 内置中英文停用词表（大小写不敏感）
var defaultStopWords = map[string]bool{
	// 中文停用词
	"的": true, "了": true, "在": true, "是": true, "我": true,
	"有": true, "和": true, "就": true, "不": true, "人": true,
	"都": true, "一": true, "一个": true, "上": true, "也": true,
	"很": true, "到": true, "说": true, "要": true, "去": true,
	"你": true, "会": true, "着": true, "没有": true, "看": true,
	"好": true, "自己": true, "这": true, "那": true, "个": true,
	"之": true, "与": true, "及": true, "或": true, "而": true,
	"以": true, "为": true, "于": true, "被": true, "把": true,
	"让": true, "给": true, "向": true, "从": true, "对": true,
	"将": true, "地": true, "得": true, "过": true,
	"吗": true, "呢": true, "吧": true, "啊": true, "哦": true,
	"嗯": true, "唉": true, "哟": true, "嘿": true, "哈": true,

	// 英文停用词
	"a": true, "i": true, "the": true, "of": true, "to": true,
	"and": true, "in": true, "is": true, "that": true, "for": true,
	"it": true, "with": true, "as": true, "was": true, "on": true,
	"by": true, "at": true, "be": true, "this": true, "have": true,
	"from": true, "or": true, "one": true, "had": true, "word": true,
	"but": true, "not": true, "what": true, "all": true, "were": true,
	"we": true, "when": true, "your": true, "can": true, "said": true,
	"there": true, "each": true, "which": true, "she": true, "do": true,
	"how": true, "their": true, "if": true, "will": true, "up": true,
	"other": true, "about": true, "out": true, "many": true, "then": true,
	"them": true, "these": true, "so": true, "some": true, "her": true,
	"would": true, "make": true, "like": true, "into": true, "him": true,
	"has": true, "two": true, "more": true, "go": true, "no": true,
	"way": true, "could": true, "my": true, "than": true, "first": true,
	"been": true, "call": true, "who": true, "its": true, "now": true,
	"find": true, "long": true, "down": true, "day": true, "did": true,
	"get": true, "come": true, "made": true, "may": true, "part": true,
	"an": true, "use": true, "his": true, "he": true, "also": true,
	"after": true, "back": true, "only": true, "know": true, "take": true,
	"year": true, "good": true, "just": true, "see": true, "over": true,
	"think": true, "work": true, "life": true, "without": true, "through": true,
}

// Keyword 表示一个关键词及其权重。
type Keyword struct {
	Word   string  `json:"word"`   // 分词后的单词/词组
	Weight float32 `json:"weight"` // TF（词频）权重，反映该词在页面中的重要程度
}

// Analyzer 封装结巴分词和语言检测器，提供线程安全的分析流水线。
type Analyzer struct {
	jieba     *gojieba.Jieba          // 结巴分词句柄（C++ 实现，非线程安全）
	detector  lingua.LanguageDetector // 语言检测器（lingua-go）
	stopWords map[string]bool         // 屏蔽词集合（标点符号、停用词等）
	mu        sync.Mutex              // 保护 jieba 调用的互斥锁
}

// New 创建一个 Analyzer 实例。
// modelPath：语言模型路径（已废弃，lingua-go 使用内置数据，无需外部文件）；
// stopWordsPath：屏蔽词 JSON 文件路径（不含文件时传入空字符串）。
func New(modelPath, stopWordsPath string) (*Analyzer, error) {
	// 初始化结巴分词（加载词典，需调用 Free 释放）
	// 词典路径：优先使用 ./dict/，否则回退到 go module 缓存
	dictPath := filepath.Join(".", "dict")
	jiebaDict := filepath.Join(dictPath, "jieba.dict.utf8")
	hmmModel := filepath.Join(dictPath, "hmm_model.utf8")
	userDict := filepath.Join(dictPath, "user.dict.utf8")
	idfPath := filepath.Join(dictPath, "idf.utf8")
	stopWords := filepath.Join(dictPath, "stop_words.utf8")

	var j *gojieba.Jieba
	if _, err := os.Stat(jiebaDict); err == nil {
		// 词典存在，使用本地路径
		j = gojieba.NewJieba(jiebaDict, hmmModel, userDict, idfPath, stopWords)
	} else {
		// 词典不存在，使用默认路径（go module 缓存）
		j = gojieba.NewJieba()
	}

	// 构建 lingua 语言检测器，覆盖所有 75 种语言（含中日韩英等）
	// MinimumRelativeDistance=0.15：降低检测阈值，提高短文本召回率
	detector := lingua.NewLanguageDetectorBuilder().
		FromAllLanguages().
		WithMinimumRelativeDistance(0.15).
		Build()

	stopWordsMap := loadStopWords(stopWordsPath)

	return &Analyzer{
		jieba:     j,
		detector:  detector,
		stopWords: stopWordsMap,
	}, nil
}

// Close 释放 Analyzer 持有的资源（主要是结巴分词的 C++ 内存）。
func (a *Analyzer) Close() {
	a.jieba.Free()
}

// loadStopWords 从 JSON 文件加载屏蔽词列表到 map 中（O(1) 查找）。
// JSON 格式：字符串数组，如 ["，", "。", "的", "了"]。
// 文件不存在或格式错误时返回内置停用词表。
func loadStopWords(path string) map[string]bool {
	// 先复制内置停用词表
	m := make(map[string]bool, len(defaultStopWords))
	for k, v := range defaultStopWords {
		m[k] = v
	}

	if path == "" {
		return m
	}
	f, err := os.Open(path)
	if err != nil {
		return m
	}
	defer f.Close()
	var words []string
	if err := json.NewDecoder(f).Decode(&words); err != nil {
		return m
	}
	for _, w := range words {
		m[strings.ToLower(w)] = true // 转为小写存储，大小写不敏感
	}
	return m
}

// Tokenize 将字符串分词为词列表。
// searchMode=true：搜索模式分词（更细粒度，适合搜索查询）；
// searchMode=false：精确模式分词（适合页面内容分析）。
// 策略：纯 ASCII 字母数字按空格切分；含中文/其他字符的片段交给结巴处理。
func (a *Analyzer) Tokenize(s string, searchMode bool) []string {
	// 超长文本截断（jieba 对极长文本处理效率下降）
	if len(s) > 10000 {
		s = s[:10000]
	}
	// 清洗非 UTF-8 字节，防止 gojieba 的 C++ 层报 "decode failed" 错误
	s = strings.ToValidUTF8(s, "")
	var result []string
	for _, part := range strings.Fields(s) {
		if isASCIIAlnum(part) {
			// 纯 ASCII 片段直接保留，不走 jieba
			result = append(result, part)
		} else {
			// 非 ASCII（含中文/日文等）：加锁后调用 jieba 分词
			a.mu.Lock()
			var tokens []string
			if searchMode {
				tokens = a.jieba.CutForSearch(part, true)
			} else {
				tokens = a.jieba.Cut(part, true)
			}
			a.mu.Unlock()
			result = append(result, tokens...)
		}
	}
	return result
}

// Normalize 标准化字符串：去除非字母数字非 CJK 字符，并转为小写。
// 用于分词后清洗，使不同大小写/全角格式的同一词归一为统一形式。
func Normalize(s string) string {
	var b strings.Builder
	for _, r := range s {
		// 保留：英文字母（a-zA-Z0-9）和 CJK 统一汉字（0x4e00-0x9fa5）
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || (r >= 0x4e00 && r <= 0x9fa5) {
			if r >= 'A' && r <= 'Z' {
				b.WriteRune(unicode.ToLower(r)) // 大写转小写
			} else {
				b.WriteRune(r) // 直接写入
			}
		}
	}
	return b.String()
}

// isValidKeyword 判断一个词是否应保留作为关键词。
// 过滤规则：
// 1. 长度 >= 2（过滤单字符）
// 2. 非纯数字
// 3. 非停用词
func isValidKeyword(word string, stopWords map[string]bool) bool {
	if word == "" {
		return false
	}
	// 检查停用词
	if stopWords[word] {
		return false
	}
	// 过滤超长词（超过 32 字符）
	if len(word) > 32 {
		return false
	}
	// 检查是否为纯数字
	isNum := true
	for _, r := range word {
		if r < '0' || r > '9' {
			isNum = false
			break
		}
	}
	if isNum {
		return false
	}
	// 过滤单字符（中文单字或英文单字母）
	runes := []rune(word)
	if len(runes) < 2 {
		return false
	}
	return true
}

// weightedTokens 对一段文本计算每个分词的 TF（词频）权重，返回 token→权重 map。
// w 为权重倍数（标题权重 1.0，描述权重 0.5，正文权重 1.0）。
func (a *Analyzer) weightedTokens(text string, w float32) map[string]float32 {
	tokens := a.Tokenize(text, false)
	d := make(map[string]float32)
	// 归一化分母：至少 8，防止只有 1-2 个词时权重过大
	n := math.Max(8, float64(len(tokens)))
	counts := make(map[string]int)
	for _, t := range tokens {
		t = Normalize(t)
		// 跳过无效关键词
		if !isValidKeyword(t, a.stopWords) {
			continue
		}
		counts[t]++
	}
	// 权重计算：min(0.2, 词频/总词数) × 权重倍数
	// 即单词权重上限 0.2，避免某个词过度主导
	for k, v := range counts {
		d[k] = float32(math.Min(0.2, float64(v)/n)) * w
	}
	return d
}

// Analyze 从标题、描述、正文三段文本中提取关键词并计算综合权重。
// 标题权重 1.0，描述权重 0.5，正文权重 1.0，三者权重相加后排序返回。
// 返回按权重降序排列的关键词切片。
func (a *Analyzer) Analyze(title, description, text string) []Keyword {
	// 分别计算三段的词权重
	maps := []map[string]float32{
		a.weightedTokens(title, 1.0),       // 标题权重最高
		a.weightedTokens(description, 0.5), // 描述权重中等
		a.weightedTokens(text, 1.0),        // 正文权重同标题
	}

	// 合并三段权重：先去重建立 key 集合
	combined := make(map[string]float32)
	for _, m := range maps {
		for k := range m {
			combined[k] = 0
		}
	}
	// 再累加各段权重
	for k := range combined {
		for _, m := range maps {
			combined[k] += m[k]
		}
	}

	// 转换为结果切片
	result := make([]Keyword, 0, len(combined))
	for k, v := range combined {
		result = append(result, Keyword{Word: k, Weight: v})
	}

	sortKeywords(result) // 按权重降序
	return result
}

// Segment 对查询字符串进行搜索模式分词并标准化（用于搜索场景）。
// 去除屏蔽词和超长词，返回有效分词列表。
func (a *Analyzer) Segment(query string, searchMode bool) []string {
	tokens := a.Tokenize(query, searchMode)
	var result []string
	for _, t := range tokens {
		t = Normalize(t)
		if !isValidKeyword(t, a.stopWords) {
			continue
		}
		result = append(result, t)
	}
	return result
}

// linguaToISO639 将 lingua 的语言枚举映射为 ISO 639-1 两字母代码。
// 只包含引擎关心的主要语种，未知语种返回空字符串。
var linguaToISO639 = map[lingua.Language]string{
	lingua.Chinese:    "zh", // 中文
	lingua.English:    "en", // 英语
	lingua.Japanese:   "ja", // 日语
	lingua.Korean:     "ko", // 韩语
	lingua.French:     "fr", // 法语
	lingua.German:     "de", // 德语
	lingua.Spanish:    "es", // 西班牙语
	lingua.Portuguese: "pt", // 葡萄牙语
	lingua.Italian:    "it", // 意大利语
	lingua.Russian:    "ru", // 俄语
	lingua.Arabic:     "ar", // 阿拉伯语
	lingua.Hindi:      "hi", // 印地语
	lingua.Dutch:      "nl", // 荷兰语
	lingua.Polish:     "pl", // 波兰语
	lingua.Swedish:    "sv", // 瑞典语
	lingua.Turkish:    "tr", // 土耳其语
	lingua.Vietnamese: "vi", // 越南语
	lingua.Thai:       "th", // 泰语
	lingua.Indonesian: "id", // 印尼语
	lingua.Malay:      "ms", // 马来语
}

// DetectLanguage 检测文本语种，返回 ISO 639-1 两字母代码，或空字符串表示无法判断。
// 截断到 2000 字符以提升检测速度（lingua 对长文本处理较慢）。
func (a *Analyzer) DetectLanguage(text string) string {
	text = strings.ReplaceAll(text, "\n", " ") // 换行替换为空格
	if len(text) > 2000 {
		text = text[:2000]
	}
	if text == "" {
		return ""
	}
	lang, exists := a.detector.DetectLanguageOf(text)
	if !exists {
		return "" // 检测失败
	}
	if code, ok := linguaToISO639[lang]; ok {
		return code // 映射为 ISO 代码
	}
	return ""
}

// ---- 排序算法 ----

// sortKeywords 对关键词切片按权重降序排列（插入排序，适合小规模数据）。
func sortKeywords(kws []Keyword) {
	for i := 1; i < len(kws); i++ {
		key := kws[i]
		j := i - 1
		for j >= 0 && kws[j].Weight < key.Weight {
			kws[j+1] = kws[j]
			j--
		}
		kws[j+1] = key
	}
}

// isASCIIAlnum 判断字符串是否全部由 ASCII 字母或数字组成。
func isASCIIAlnum(s string) bool {
	for _, r := range s {
		if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) {
			return false
		}
	}
	return len(s) > 0
}
