package engine

import (
	"bytes"
	"fmt"
	"maps"
	"path/filepath"
	"sort"

	dawg "github.com/iliadenisov/dafsa"
)

// WordDiff is the set difference between two dictionaries of one variant: the
// words present only in the new dictionary (Added) and only in the old one
// (Removed), decoded to characters and each sorted by the variant's alphabet
// order. It drives the admin dictionary-update preview.
type WordDiff struct {
	Added   []string
	Removed []string
}

// DictFiles returns a copy of the variant-to-committed-DAWG-filename map. It lets
// callers outside the package (the dictionary-admin upload validation) check an
// archive against the expected file set without sharing the engine's own map.
func DictFiles() map[Variant]string {
	out := make(map[Variant]string, len(dictFiles))
	maps.Copy(out, dictFiles)
	return out
}

// OpenFinder loads the committed DAWG of variant v from dir and returns its
// finder. The caller owns the finder and must Close it. It backs enumeration of
// a staged, not-yet-registered dictionary version.
func OpenFinder(dir string, v Variant) (dawg.Finder, error) {
	name, ok := dictFiles[v]
	if !ok {
		return nil, fmt.Errorf("%w: %d", ErrUnknownVariant, v)
	}
	path := filepath.Join(dir, name)
	finder, err := dawg.Load(path)
	if err != nil {
		return nil, fmt.Errorf("engine: load %s dictionary from %s: %w", v, path, err)
	}
	return finder, nil
}

// Finder returns the loaded finder for the (variant, version) pair so its words
// can be enumerated, or ErrUnknownVariant / ErrUnknownVersion when that
// dictionary is not resident. The finder remains owned by the registry; callers
// must not Close it.
func (r *Registry) Finder(v Variant, version string) (dawg.Finder, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	versions, ok := r.entries[v]
	if !ok {
		return nil, fmt.Errorf("%w: %s", ErrUnknownVariant, v)
	}
	e, ok := versions[version]
	if !ok {
		return nil, fmt.Errorf("%w: %s/%s", ErrUnknownVersion, v, version)
	}
	return e.finder, nil
}

// DiffWords compares the old and new finders of variant v and returns the words
// added and removed, decoded through v's alphabet. Both dictionaries are
// enumerated as alphabet-index bytes and merged; only the differing words are
// decoded, so a full-dictionary comparison does not materialise two large string
// sets.
func DiffWords(v Variant, old, updated dawg.Finder) (WordDiff, error) {
	rs, ok := v.ruleset()
	if !ok {
		return WordDiff{}, fmt.Errorf("%w: %d", ErrUnknownVariant, v)
	}
	oldWords := collectWords(old)
	newWords := collectWords(updated)

	var diff WordDiff
	i, j := 0, 0
	for i < len(oldWords) && j < len(newWords) {
		switch cmp := bytes.Compare(oldWords[i], newWords[j]); {
		case cmp == 0:
			i++
			j++
		case cmp < 0:
			s, err := rs.Alphabet.Decode(oldWords[i])
			if err != nil {
				return WordDiff{}, fmt.Errorf("engine: decode %s removed word: %w", v, err)
			}
			diff.Removed = append(diff.Removed, s)
			i++
		default:
			s, err := rs.Alphabet.Decode(newWords[j])
			if err != nil {
				return WordDiff{}, fmt.Errorf("engine: decode %s added word: %w", v, err)
			}
			diff.Added = append(diff.Added, s)
			j++
		}
	}
	for ; i < len(oldWords); i++ {
		s, err := rs.Alphabet.Decode(oldWords[i])
		if err != nil {
			return WordDiff{}, fmt.Errorf("engine: decode %s removed word: %w", v, err)
		}
		diff.Removed = append(diff.Removed, s)
	}
	for ; j < len(newWords); j++ {
		s, err := rs.Alphabet.Decode(newWords[j])
		if err != nil {
			return WordDiff{}, fmt.Errorf("engine: decode %s added word: %w", v, err)
		}
		diff.Added = append(diff.Added, s)
	}
	return diff, nil
}

// collectWords enumerates every complete word of f as a copy of its
// alphabet-index bytes, sorted ascending. The dawg already enumerates in index
// order; the explicit sort makes the merge in DiffWords robust to that being an
// implementation detail.
func collectWords(f dawg.Finder) [][]byte {
	out := make([][]byte, 0, f.NumAdded())
	f.EnumerateB(func(_ int, word []byte, final bool) dawg.EnumerationResult {
		if final && len(word) > 0 {
			cp := make([]byte, len(word))
			copy(cp, word)
			out = append(out, cp)
		}
		return dawg.Continue
	})
	sort.Slice(out, func(a, b int) bool { return bytes.Compare(out[a], out[b]) < 0 })
	return out
}
