//
//  Copyright 2026 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 wordnet provides a Go implementation of NLTK's WordNet synsets functionality.
// This implementation reads WordNet 3.0 database files and provides synonym set lookup.
package nlp

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
)

// POS constants for WordNet parts of speech
const (
	NOUN = "n"
	VERB = "v"
	ADJ  = "a"
	ADV  = "r"
)

// Morphy substitution rules for each POS
var morphologicalSubstitutions = map[string][][2]string{
	NOUN: {
		{"s", ""},
		{"ses", "s"},
		{"ves", "f"},
		{"xes", "x"},
		{"zes", "z"},
		{"ches", "ch"},
		{"shes", "sh"},
		{"men", "man"},
		{"ies", "y"},
	},
	VERB: {
		{"s", ""},
		{"ies", "y"},
		{"es", "e"},
		{"es", ""},
		{"ed", "e"},
		{"ed", ""},
		{"ing", "e"},
		{"ing", ""},
	},
	ADJ: {
		{"er", ""},
		{"est", ""},
		{"er", "e"},
		{"est", "e"},
	},
	ADV: {},
}

// File suffix mapping for POS
var fileMap = map[string]string{
	NOUN: "noun",
	VERB: "verb",
	ADJ:  "adj",
	ADV:  "adv",
}

// Synset represents a WordNet synset (synonym set)
type Synset struct {
	Name       string
	POS        string
	Offset     int
	Lemmas     []string
	Definition string
	Examples   []string
}

// WordNet is the main struct for WordNet operations
type WordNet struct {
	wordNetDir          string
	lemmaPosOffsetMap   map[string]map[string][]int
	exceptionMap        map[string]map[string][]string
	dataFileCache       map[string]*os.File
	dataFileCacheOffset map[string]int64
	fileMutexes         map[string]*sync.Mutex // Mutex for each POS to ensure concurrency safety
}

// NewWordNet creates a new WordNet instance with the given WordNet directory
func NewWordNet(wordNetDir string) (*WordNet, error) {
	wn := &WordNet{
		wordNetDir:          wordNetDir,
		lemmaPosOffsetMap:   make(map[string]map[string][]int),
		exceptionMap:        make(map[string]map[string][]string),
		dataFileCache:       make(map[string]*os.File),
		dataFileCacheOffset: make(map[string]int64),
		fileMutexes:         make(map[string]*sync.Mutex),
	}

	// Initialize exception maps for all POS
	for pos := range fileMap {
		wn.exceptionMap[pos] = make(map[string][]string)
	}

	// Load exception files
	if err := wn.loadExceptionMaps(); err != nil {
		return nil, fmt.Errorf("failed to load exception maps: %w", err)
	}

	// Load lemma pos offset map
	if err := wn.loadLemmaPosOffsetMap(); err != nil {
		return nil, fmt.Errorf("failed to load lemma pos offset map: %w", err)
	}

	return wn, nil
}

// Close closes all cached file handles
func (wn *WordNet) Close() {
	for pos, f := range wn.dataFileCache {
		if mutex, ok := wn.fileMutexes[pos]; ok {
			mutex.Lock()
			f.Close()
			mutex.Unlock()
		} else {
			f.Close()
		}
	}
}

// loadExceptionMaps loads the .exc files for each POS
func (wn *WordNet) loadExceptionMaps() error {
	for pos, suffix := range fileMap {
		filename := filepath.Join(wn.wordNetDir, suffix+".exc")
		file, err := os.Open(filename)
		if err != nil {
			// It's okay if the file doesn't exist for some POS
			continue
		}
		defer file.Close()

		scanner := bufio.NewScanner(file)
		for scanner.Scan() {
			line := scanner.Text()
			fields := strings.Fields(line)
			if len(fields) >= 2 {
				// First field is the inflected form, rest are base forms
				wn.exceptionMap[pos][fields[0]] = fields[1:]
			}
		}
		if err := scanner.Err(); err != nil {
			return fmt.Errorf("error reading %s: %w", filename, err)
		}
	}
	return nil
}

// loadLemmaPosOffsetMap loads the index files for each POS
func (wn *WordNet) loadLemmaPosOffsetMap() error {
	for _, suffix := range fileMap {
		filename := filepath.Join(wn.wordNetDir, "index."+suffix)
		file, err := os.Open(filename)
		if err != nil {
			return fmt.Errorf("failed to open %s: %w", filename, err)
		}
		defer file.Close()

		scanner := bufio.NewScanner(file)
		for scanner.Scan() {
			line := scanner.Text()

			// Skip license header lines (lines starting with space)
			if len(line) == 0 || line[0] == ' ' {
				continue
			}

			fields := strings.Fields(line)
			if len(fields) < 6 {
				continue
			}

			// Parse index file format:
			// lemma pos n_synsets n_pointers [pointers] n_senses n_ranked_synsets [synset_offsets...]
			lemma := strings.ToLower(fields[0])
			filePos := fields[1]
			nSynsets, err := strconv.Atoi(fields[2])
			if err != nil {
				continue
			}
			nPointers, err := strconv.Atoi(fields[3])
			if err != nil {
				continue
			}

			// Calculate field positions
			fieldIdx := 4

			// Skip pointer symbols
			for i := 0; i < nPointers && fieldIdx < len(fields); i++ {
				fieldIdx++
			}

			// Read n_senses and n_ranked_synsets
			if fieldIdx >= len(fields) {
				continue
			}
			_, err = strconv.Atoi(fields[fieldIdx]) // n_senses
			if err != nil {
				continue
			}
			fieldIdx++

			if fieldIdx >= len(fields) {
				continue
			}
			_, err = strconv.Atoi(fields[fieldIdx]) // n_ranked_synsets
			if err != nil {
				continue
			}
			fieldIdx++

			// Read synset offsets
			var offsets []int
			for i := 0; i < nSynsets && fieldIdx < len(fields); i++ {
				offset, err := strconv.Atoi(fields[fieldIdx])
				if err != nil {
					continue
				}
				offsets = append(offsets, offset)
				fieldIdx++
			}

			// Store in map
			if wn.lemmaPosOffsetMap[lemma] == nil {
				wn.lemmaPosOffsetMap[lemma] = make(map[string][]int)
			}
			wn.lemmaPosOffsetMap[lemma][filePos] = offsets
		}
		if err := scanner.Err(); err != nil {
			return fmt.Errorf("error reading %s: %w", filename, err)
		}
	}
	return nil
}

// morphy performs morphological analysis to find base forms of a word
func (wn *WordNet) morphy(form string, pos string, checkExceptions bool) []string {
	form = strings.ToLower(form)
	exceptions := wn.exceptionMap[pos]
	substitutions := morphologicalSubstitutions[pos]

	// Helper function to apply substitution rules
	applyRules := func(forms []string) []string {
		var results []string
		for _, f := range forms {
			for _, sub := range substitutions {
				old, new := sub[0], sub[1]
				if strings.HasSuffix(f, old) {
					base := f[:len(f)-len(old)] + new
					results = append(results, base)
				}
			}
		}
		return results
	}

	// Helper function to filter forms that exist in WordNet
	filterForms := func(forms []string) []string {
		var results []string
		seen := make(map[string]bool)
		for _, f := range forms {
			if posMap, ok := wn.lemmaPosOffsetMap[f]; ok {
				if _, hasPos := posMap[pos]; hasPos {
					if !seen[f] {
						results = append(results, f)
						seen[f] = true
					}
				}
			}
		}
		return results
	}

	var forms []string
	if checkExceptions {
		if baseForms, ok := exceptions[form]; ok {
			forms = baseForms
		}
	}

	// If no exception found, apply rules
	if len(forms) == 0 {
		forms = applyRules([]string{form})
	}

	// Filter to keep only valid forms, also check original form
	return filterForms(append([]string{form}, forms...))
}

// getDataFile returns the data file for a given POS, with caching
func (wn *WordNet) getDataFile(pos string) (*os.File, *sync.Mutex, error) {
	if pos == "s" { // Adjective satellite uses the same file as adjective
		pos = ADJ
	}

	// Get or create mutex for this POS
	mutex, exists := wn.fileMutexes[pos]
	if !exists {
		mutex = &sync.Mutex{}
		wn.fileMutexes[pos] = mutex
	}

	if file, ok := wn.dataFileCache[pos]; ok {
		return file, mutex, nil
	}

	suffix, ok := fileMap[pos]
	if !ok {
		return nil, nil, fmt.Errorf("unknown POS: %s", pos)
	}

	filename := filepath.Join(wn.wordNetDir, "data."+suffix)
	file, err := os.Open(filename)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to open %s: %w", filename, err)
	}

	wn.dataFileCache[pos] = file
	return file, mutex, nil
}

// parseDataLine parses a line from a data file and returns a Synset
func parseDataLine(line string, pos string) (*Synset, error) {
	// Data file format:
	// synset_offset lex_filenum ss_type w_cnt word lex_id [word lex_id...] p_cnt [ptr_symbol synset_offset pos src_trgt...] [frames...] | gloss

	parts := strings.SplitN(line, "|", 2)
	if len(parts) != 2 {
		return nil, fmt.Errorf("invalid line format: no gloss separator")
	}

	dataPart := strings.TrimSpace(parts[0])
	glossPart := strings.TrimSpace(parts[1])

	// Parse gloss to get definition and examples
	var definition string
	var examples []string

	// Remove quotes from examples
	gloss := glossPart
	for {
		start := strings.Index(gloss, "\"")
		if start == -1 {
			break
		}
		end := strings.Index(gloss[start+1:], "\"")
		if end == -1 {
			break
		}
		end += start + 1

		example := gloss[start+1 : end]
		if len(examples) == 0 && start > 0 {
			definition = strings.TrimSpace(gloss[:start])
		}
		examples = append(examples, example)
		gloss = gloss[end+1:]
	}

	if definition == "" {
		definition = strings.Trim(glossPart, "; ")
		// Remove quoted examples from definition
		definition = regexpRemoveQuotes(definition)
	}

	// Final cleanup: trim trailing semicolon and whitespace to match Python NLTK
	definition = strings.TrimRight(definition, "; ")

	// Parse data part
	fields := strings.Fields(dataPart)
	if len(fields) < 4 {
		return nil, fmt.Errorf("invalid data line: too few fields")
	}

	offset, err := strconv.Atoi(fields[0])
	if err != nil {
		return nil, fmt.Errorf("invalid offset: %w", err)
	}

	// lexFilenum := fields[1]  // Not used currently
	ssType := fields[2]

	wCnt, err := strconv.ParseInt(fields[3], 16, 32)
	if err != nil {
		return nil, fmt.Errorf("invalid word count: %w", err)
	}

	// Parse lemmas
	var lemmas []string
	fieldIdx := 4
	for i := 0; i < int(wCnt) && fieldIdx+1 < len(fields); i++ {
		lemma := fields[fieldIdx]
		// Remove syntactic marker if present (e.g., "(a)" or "(p)")
		if idx := strings.Index(lemma, "("); idx != -1 {
			lemma = lemma[:idx]
		}
		// Keep original case for lemmas (Python NLTK preserves case)
		lemmas = append(lemmas, lemma)
		fieldIdx += 2 // skip lex_id
	}

	if len(lemmas) == 0 {
		return nil, fmt.Errorf("no lemmas found")
	}

	// Build synset name from first lemma (Python uses lowercase in synset name)
	senseIndex := 1 // Default to 1, would need to look up in index for actual sense number
	name := fmt.Sprintf("%s.%s.%02d", strings.ToLower(lemmas[0]), ssType, senseIndex)

	return &Synset{
		Name:       name,
		POS:        ssType,
		Offset:     offset,
		Lemmas:     lemmas,
		Definition: definition,
		Examples:   examples,
	}, nil
}

// regexpRemoveQuotes removes quoted strings from text (simplified version)
func regexpRemoveQuotes(s string) string {
	var result strings.Builder
	inQuote := false
	for _, ch := range s {
		if ch == '"' {
			inQuote = !inQuote
			continue
		}
		if !inQuote {
			result.WriteRune(ch)
		}
	}
	return strings.TrimSpace(strings.Trim(result.String(), "; "))
}

// synsetFromPosAndOffset retrieves a synset by POS and byte offset
func (wn *WordNet) synsetFromPosAndOffset(pos string, offset int) (*Synset, error) {
	file, mutex, err := wn.getDataFile(pos)
	if err != nil {
		return nil, err
	}

	// Lock only for Seek and Read operations to minimize critical section
	mutex.Lock()

	// Seek to the offset
	_, err = file.Seek(int64(offset), 0)
	if err != nil {
		mutex.Unlock()
		return nil, fmt.Errorf("failed to seek to offset %d: %w", offset, err)
	}

	reader := bufio.NewReader(file)
	line, err := reader.ReadString('\n')
	mutex.Unlock() // Release lock immediately after reading

	if err != nil {
		return nil, fmt.Errorf("failed to read line at offset %d: %w", offset, err)
	}

	//if len(line) < 8 {
	//	fmt.Println(line)
	//}

	// Verify the offset matches
	lineOffset := strings.TrimSpace(line[:8])
	expectedOffset := fmt.Sprintf("%08d", offset)
	if lineOffset != expectedOffset {
		return nil, fmt.Errorf("offset mismatch: expected %s, got %s", expectedOffset, lineOffset)
	}

	synset, err := parseDataLine(line, pos)
	if err != nil {
		return nil, err
	}

	// Calculate the correct sense number by looking up the offset in the index
	// This operation only accesses memory map, no need for file lock
	senseNum := wn.findSenseNumber(synset.Lemmas[0], pos, offset)
	if senseNum > 0 {
		synset.Name = fmt.Sprintf("%s.%s.%02d", synset.Lemmas[0], synset.POS, senseNum)
	}

	return synset, nil
}

// findSenseNumber finds the sense number for a lemma in a given synset
func (wn *WordNet) findSenseNumber(lemma string, pos string, offset int) int {
	lemma = strings.ToLower(lemma)
	if posMap, ok := wn.lemmaPosOffsetMap[lemma]; ok {
		if offsets, hasPos := posMap[pos]; hasPos {
			for i, off := range offsets {
				if off == offset {
					return i + 1 // sense numbers are 1-indexed
				}
			}
		}
	}
	return 1 // Default to 1 if not found
}

// Synsets returns all synsets for a given lemma and optional POS.
// If pos is empty, all parts of speech are searched.
// This is the main function equivalent to NLTK's wordnet.synsets()
func (wn *WordNet) Synsets(lemma string, pos string) []*Synset {
	lemma = strings.ToLower(lemma)

	var poses []string
	if pos == "" {
		poses = []string{NOUN, VERB, ADJ, ADV}
	} else {
		poses = []string{pos}
	}

	var results []*Synset
	seen := make(map[string]bool)

	for _, p := range poses {
		// Get morphological forms
		forms := wn.morphy(lemma, p, true)

		for _, form := range forms {
			if posMap, ok := wn.lemmaPosOffsetMap[form]; ok {
				if offsets, hasPos := posMap[p]; hasPos {
					for _, offset := range offsets {
						// Create unique key to avoid duplicates
						key := fmt.Sprintf("%s-%d", p, offset)
						if !seen[key] {
							seen[key] = true
							synset, err := wn.synsetFromPosAndOffset(p, offset)
							if err == nil {
								results = append(results, synset)
							}
						}
					}
				}
			}
		}
	}

	return results
}

// Name returns the synset name (e.g., "dog.n.01")
func (s *Synset) NameStr() string {
	return s.Name
}

// String returns a string representation of the synset
func (s *Synset) String() string {
	return fmt.Sprintf("Synset('%s')", s.Name)
}
