// Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package nlp

import (
	"encoding/json"
	"math"
	"os"
	"path/filepath"
	"ragflow/internal/common"
	"regexp"
	"strconv"
	"strings"

	"ragflow/internal/tokenizer"

	"go.uber.org/zap"
)

// TermWeightDealer calculates term weights for text processing
// Reference: rag/nlp/term_weight.py
type TermWeightDealer struct {
	stopWords map[string]struct{}
	ne        map[string]string // named entities
	df        map[string]int    // document frequency
}

// TermWeight represents a term and its weight
type TermWeight struct {
	Term   string
	Weight float64
}

// NewTermWeightDealer creates a new TermWeightDealer
func NewTermWeightDealer(resPath string) *TermWeightDealer {
	d := &TermWeightDealer{
		stopWords: initStopWords(),
		ne:        make(map[string]string),
		df:        make(map[string]int),
	}

	// Load named entity dictionary
	if resPath == "" {
		resPath = "rag/res"
	}

	nerPath := filepath.Join(resPath, "ner.json")
	if data, err := os.ReadFile(nerPath); err == nil {
		if err := json.Unmarshal(data, &d.ne); err != nil {
			common.Warn("Failed to load ner.json", zap.Error(err))
		}
	} else {
		common.Warn("Failed to load ner.json", zap.Error(err))
	}

	// Load term frequency dictionary
	freqPath := filepath.Join(resPath, "term.freq")
	d.df = loadDict(freqPath)

	return d
}

// initStopWords initializes the stop words set
func initStopWords() map[string]struct{} {
	words := []string{
		"请问", "您", "你", "我", "他", "是", "的", "就", "有", "于",
		"及", "即", "在", "为", "最", "有", "从", "以", "了", "将",
		"与", "吗", "吧", "中", "#", "什么", "怎么", "哪个", "哪些",
		"啥", "相关",
	}
	stopWords := make(map[string]struct{}, len(words))
	for _, w := range words {
		stopWords[w] = struct{}{}
	}
	return stopWords
}

// loadDict loads a dictionary file
// Format: term\tfreq or just term
func loadDict(fnm string) map[string]int {
	res := make(map[string]int)
	data, err := os.ReadFile(fnm)
	if err != nil {
		common.Warn("Failed to load dictionary", zap.String("file", fnm), zap.Error(err))
		return res
	}

	lines := strings.Split(string(data), "\n")
	totalFreq := 0
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		arr := strings.Split(line, "\t")
		if len(arr) >= 2 {
			if freq, err := strconv.Atoi(arr[1]); err == nil {
				res[arr[0]] = freq
				totalFreq += freq
			}
		} else {
			res[arr[0]] = 0
		}
	}

	// If no frequencies, return as set (all 0)
	if totalFreq == 0 {
		return res
	}
	return res
}

// Pretoken preprocesses and tokenizes text
// Reference: term_weight.py L92-114
func (d *TermWeightDealer) Pretoken(txt string, num bool, stpwd bool) []string {
	patt := `[~—\t @#%!<>,\.\?":;'\{\}\[\]_=\(\)\|，。？》•●○↓《；'：""【¥ 】…￥！、·（）×\` + "`" + `&/「」\]`

	res := []string{}
	tokenized, err := tokenizer.Tokenize(txt)
	if err != nil {
		// Fallback to simple split
		tokenized = txt
	}

	for _, t := range strings.Fields(tokenized) {
		tk := t
		// Check stop words
		if stpwd {
			if _, isStop := d.stopWords[tk]; isStop {
				continue
			}
		}
		// Check single digit (unless num is true)
		if matched, _ := regexp.MatchString("^[0-9]$", tk); matched && !num {
			continue
		}
		// Check patterns
		if matched, _ := regexp.MatchString(patt, t); matched {
			tk = "#"
		}
		if tk != "#" && tk != "" {
			res = append(res, tk)
		}
	}
	return res
}

// TokenMerge merges short tokens into phrases
// Reference: term_weight.py L116-143
func (d *TermWeightDealer) TokenMerge(tks []string) []string {
	oneTerm := func(t string) bool {
		// Use rune count for proper Unicode handling
		runeCount := len([]rune(t))
		if runeCount == 1 {
			return true
		}
		// Match 1-2 alphanumeric characters
		matched, _ := regexp.MatchString("^[0-9a-z]{1,2}$", t)
		return matched
	}

	if len(tks) == 0 {
		return []string{}
	}

	res := []string{}
	i := 0
	for i < len(tks) {
		// Special case: first term is single char and next is multi-char Chinese
		if i == 0 && len(tks) > 1 && oneTerm(tks[i]) {
			nextLen := len([]rune(tks[i+1]))
			isNextMultiChar := nextLen > 1
			isNextNotAlnum, _ := regexp.MatchString("^[0-9a-zA-Z]", tks[i+1])
			if isNextMultiChar && !isNextNotAlnum {
				res = append(res, tks[0]+" "+tks[1])
				i = 2
				continue
			}
		}

		j := i
		for j < len(tks) && tks[j] != "" {
			if _, isStop := d.stopWords[tks[j]]; isStop {
				break
			}
			if !oneTerm(tks[j]) {
				break
			}
			j++
		}

		if j-i > 1 {
			if j-i < 5 {
				res = append(res, strings.Join(tks[i:j], " "))
				i = j
			} else {
				// Split into pairs for 5+ consecutive short tokens
				for k := i; k < j; k += 2 {
					if k+1 < j {
						res = append(res, tks[k]+" "+tks[k+1])
					} else {
						res = append(res, tks[k])
					}
				}
				i = j
			}
		} else {
			if len(tks[i]) > 0 {
				res = append(res, tks[i])
			}
			i++
		}
	}

	// Filter empty strings
	filtered := []string{}
	for _, t := range res {
		if t != "" {
			filtered = append(filtered, t)
		}
	}
	return filtered
}

// Ner gets named entity type for a term
// Reference: term_weight.py L145-150
func (d *TermWeightDealer) Ner(t string) string {
	if d.ne == nil {
		return ""
	}
	if res, ok := d.ne[t]; ok {
		return res
	}
	return ""
}

// Split splits text into tokens, merging consecutive English words
// Reference: term_weight.py L152-161
func (d *TermWeightDealer) Split(txt string) []string {
	if txt == "" {
		return []string{""}
	}

	tks := []string{}
	// Normalize spaces (tabs and multiple spaces -> single space)
	txt = regexp.MustCompile("[ \\t]+").ReplaceAllString(txt, " ")
	txt = strings.TrimSpace(txt)

	for _, t := range strings.Split(txt, " ") {
		t = strings.TrimSpace(t)
		if t == "" {
			continue
		}
		if len(tks) > 0 {
			prevEndsWithLetter, _ := regexp.MatchString(".*[a-zA-Z]$", tks[len(tks)-1])
			currEndsWithLetter, _ := regexp.MatchString(".*[a-zA-Z]$", t)
			prevNE := d.ne[tks[len(tks)-1]]
			currNE := d.ne[t]
			if prevEndsWithLetter && currEndsWithLetter &&
				currNE != "func" && prevNE != "func" {
				tks[len(tks)-1] = tks[len(tks)-1] + " " + t
				continue
			}
		}
		tks = append(tks, t)
	}
	return tks
}

// Weights calculates weights for tokens
// Reference: term_weight.py L163-246
func (d *TermWeightDealer) Weights(tks []string, preprocess bool) []TermWeight {
	numPattern := regexp.MustCompile("^[0-9,.]{2,}$")
	shortLetterPattern := regexp.MustCompile("^[a-z]{1,2}$")
	numSpacePattern := regexp.MustCompile("^[0-9. -]{2,}$")
	letterPattern := regexp.MustCompile("^[a-z. -]+$")

	// ner weight function
	nerWeight := func(t string) float64 {
		if numPattern.MatchString(t) {
			return 2
		}
		if shortLetterPattern.MatchString(t) {
			return 0.01
		}
		if d.ne == nil {
			return 1
		}
		if neType, ok := d.ne[t]; ok {
			weights := map[string]float64{
				"toxic": 2, "func": 1, "corp": 3, "loca": 3,
				"sch": 3, "stock": 3, "firstnm": 1,
			}
			if w, exists := weights[neType]; exists {
				return w
			}
		}
		return 1
	}

	// postag weight function using real POS tagger
	postagWeight := func(t string) float64 {
		tag := tokenizer.GetTermTag(t)
		// Map POS tags to weights (matching Python implementation)
		if tag == "r" || tag == "c" || tag == "d" {
			return 0.3
		}
		if tag == "ns" || tag == "nt" {
			return 3
		}
		if tag == "n" {
			return 2
		}
		// Fallback to heuristic for terms without tags
		if matched, _ := regexp.MatchString("^[0-9-]+", tag); matched {
			return 2
		}
		return 1
	}

	// freq function using real frequency dictionary
	var freq func(t string) float64
	freq = func(t string) float64 {
		if numSpacePattern.MatchString(t) {
			return 3
		}
		// Use tokenizer's freq function
		s := tokenizer.GetTermFreq(t)
		if s == 0 && letterPattern.MatchString(t) {
			return 300
		}
		if s == 0 && len([]rune(t)) >= 4 {
			// Try fine-grained tokenization
			fgTokens, _ := tokenizer.FineGrainedTokenize(t)
			tokens := strings.Fields(fgTokens)

			// Filter: only keep tokens with length > 1
			var filteredTokens []string
			for _, tt := range tokens {
				if len([]rune(tt)) > 1 {
					filteredTokens = append(filteredTokens, tt)
				}
			}

			var validTokens []float64
			if len(filteredTokens) > 1 {
				for _, tt := range filteredTokens {
					f := freq(tt)
					validTokens = append(validTokens, f)
				}

				minVal := validTokens[0]
				for _, v := range validTokens[1:] {
					if v < minVal {
						minVal = v
					}
				}
				return minVal / 6.0
			}

			// Default frequency
			return 10
		}
		return math.Max(float64(s), 10)
	}

	// df function
	var df func(t string) float64
	df = func(t string) float64 {
		if numSpacePattern.MatchString(t) {
			return 5
		}
		if v, ok := d.df[t]; ok {
			return float64(v) + 3
		}
		if letterPattern.MatchString(t) {
			return 300
		}
		if len([]rune(t)) >= 4 {
			// Use fine-grained tokenization
			fgTokens, _ := tokenizer.FineGrainedTokenize(t)
			tokens := strings.Fields(fgTokens)

			// Filter: only keep tokens with length > 1
			var filteredTokens []string
			for _, tt := range tokens {
				if len([]rune(tt)) > 1 {
					filteredTokens = append(filteredTokens, tt)
				}
			}

			var validTokens []float64
			if len(filteredTokens) > 1 {
				for _, tt := range filteredTokens {
					f := df(tt)
					validTokens = append(validTokens, f)
				}

				minVal := validTokens[0]
				for _, v := range validTokens[1:] {
					if v < minVal {
						minVal = v
					}
				}
				return math.Max(3, minVal/6.0)
			}
		}
		return 3
	}

	// idf function
	// Uses common.PyLog10 (C library's log10 via cgo) instead of
	// math.Log10 to match Python's math.log10 exactly. Go's pure-Go
	// math.Log10 can differ by 1 ULP from glibc's log10, causing parity
	// test failures.
	idf := func(s, N float64) float64 {
		arg := 10 + ((N - s + 0.5) / (s + 0.5))
		result := common.PyLog10(arg)
		return result
	}

	tw := []TermWeight{}

	if !preprocess {
		// Direct calculation without preprocessing
		idf1Vals := make([]float64, len(tks))
		idf2Vals := make([]float64, len(tks))
		nerPosVals := make([]float64, len(tks))

		for i, t := range tks {
			//fmt.Println("index:", i, "term:", t)
			idf1Vals[i] = idf(freq(t), 10000000)
			idf2Vals[i] = idf(df(t), 1000000000)
			nerPosVals[i] = nerWeight(t) * postagWeight(t)
		}

		wts := make([]float64, len(tks))
		for i := range tks {
			wts[i] = (0.3*idf1Vals[i] + 0.7*idf2Vals[i]) * nerPosVals[i]
		}

		for i, t := range tks {
			tw = append(tw, TermWeight{Term: t, Weight: wts[i]})
		}
	} else {
		// With preprocessing
		for _, tk := range tks {
			tokens := d.Pretoken(tk, true, true)
			tt := d.TokenMerge(tokens)
			if len(tt) == 0 {
				continue
			}

			idf1Vals := make([]float64, len(tt))
			idf2Vals := make([]float64, len(tt))
			nerPosVals := make([]float64, len(tt))

			for i, t := range tt {
				idf1Vals[i] = idf(freq(t), 10000000)
				idf2Vals[i] = idf(df(t), 1000000000)
				nerPosVals[i] = nerWeight(t) * postagWeight(t)
			}

			wts := make([]float64, len(tt))
			for i := range tt {
				wts[i] = (0.3*idf1Vals[i] + 0.7*idf2Vals[i]) * nerPosVals[i]
			}

			for i, t := range tt {
				tw = append(tw, TermWeight{Term: t, Weight: wts[i]})
			}
		}
	}

	// Normalize weights
	if len(tw) == 0 {
		return tw
	}

	// Use PairwiseSum to match Python's np.sum() which uses pairwise summation
	weightBuf := make([]float64, len(tw))
	for i, twItem := range tw {
		weightBuf[i] = twItem.Weight
	}
	S := common.PairwiseSum(weightBuf)

	if S > 0 {
		for i := range tw {
			tw[i].Weight = tw[i].Weight / S
		}
	}

	return tw
}

// GetStopWords returns the stop words set
func (d *TermWeightDealer) GetStopWords() map[string]struct{} {
	return d.stopWords
}

// GetNE returns the named entity dictionary
func (d *TermWeightDealer) GetNE() map[string]string {
	return d.ne
}

// GetDF returns the document frequency dictionary
func (d *TermWeightDealer) GetDF() map[string]int {
	return d.df
}
