package engine

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"

	dawg "github.com/iliadenisov/dafsa"

	"gitea.iliadenisov.ru/developer/scrabble-solver/scrabble"
)

// dictFiles maps each variant to its committed DAWG filename, as built by
// scrabble-solver and delivered in the dictionary directory.
var dictFiles = map[Variant]string{
	VariantEnglish:         "en_sowpods.dawg",
	VariantRussianScrabble: "ru_scrabble.dawg",
	VariantErudit:          "ru_erudit.dawg",
}

// entry is one resident dictionary: the loaded finder, the solver built over it
// and the file it was loaded from. The finder is retained so Close can release
// it; path is retained so the raw bytes can be re-read for the client download.
type entry struct {
	finder dawg.Finder
	solver *scrabble.Solver
	path   string
}

// Registry holds the dictionaries resident in memory, addressed by variant and
// dictionary version, and the solvers built over them. Several versions of a
// variant may be resident at once; a game pins the version it started on. The
// admin reload flow registers a new version through Load.
// Registry is safe for concurrent use.
type Registry struct {
	mu      sync.RWMutex
	entries map[Variant]map[string]entry
	latest  map[Variant]string
}

// NewRegistry constructs an empty Registry. Use Load or Open to populate it.
func NewRegistry() *Registry {
	return &Registry{
		entries: make(map[Variant]map[string]entry),
		latest:  make(map[Variant]string),
	}
}

// Open builds a Registry by loading, at dictionary version, the committed DAWG of
// every requested variant (or all variants when none are named) from dir. It
// fails if a variant is unknown or its file is missing or unreadable; a
// partially loaded registry is closed before the error is returned.
func Open(dir, version string, variants ...Variant) (*Registry, error) {
	if len(variants) == 0 {
		variants = Variants()
	}
	r := NewRegistry()
	for _, v := range variants {
		if err := r.Load(v, version, dir); err != nil {
			_ = r.Close()
			return nil, err
		}
	}
	return r, nil
}

// OpenWithVersions builds a Registry by loading the boot version from the flat
// dir (every variant, as Open) and then every additional version held in an
// immediate subdirectory of dir: a subdirectory named V contributes, under
// version V, the variants whose committed DAWG it carries. This is the
// restart-side of the admin dictionary reload — a version reloaded into dir/<V>/
// at runtime is resident again after a restart. The flat dir's version is resolved
// from its .seed_version marker (see resolveSeedVersion): a fresh dir records
// bootVersion, an already-seeded dir keeps its recorded label and ignores bootVersion,
// so a bumped build seed never relabels live bytes. A subdirectory named like the
// resolved seed version is skipped (the flat dir already is it). A partially loaded
// registry is closed before any error is returned.
func OpenWithVersions(dir, bootVersion string) (*Registry, error) {
	// Resolve the flat dir's version from its seed marker first: on an already-seeded
	// volume the marker wins and bootVersion is ignored, so a bumped build seed cannot
	// relabel live bytes (see resolveSeedVersion).
	seed, err := resolveSeedVersion(dir, bootVersion)
	if err != nil {
		return nil, err
	}
	r, err := Open(dir, seed)
	if err != nil {
		return nil, err
	}
	entries, err := os.ReadDir(dir)
	if err != nil {
		_ = r.Close()
		return nil, fmt.Errorf("engine: scan dictionary dir %s: %w", dir, err)
	}
	for _, e := range entries {
		// Skip non-directories, the resolved seed version (already loaded as the flat
		// dir) and dot-prefixed directories (the upload staging area, dir/.staging/).
		if !e.IsDir() || e.Name() == seed || strings.HasPrefix(e.Name(), ".") {
			continue
		}
		if _, err := r.LoadAvailable(filepath.Join(dir, e.Name()), e.Name()); err != nil {
			_ = r.Close()
			return nil, err
		}
	}
	return r, nil
}

// Load reads the committed DAWG of variant from dir, builds a solver over it and
// registers it under version. Reloading the same (variant, version) replaces the
// previous entry, closing its finder. The most recently loaded version of a
// variant becomes its latest.
func (r *Registry) Load(v Variant, version, dir string) error {
	rs, ok := v.ruleset()
	if !ok {
		return fmt.Errorf("%w: %d", ErrUnknownVariant, v)
	}
	path := filepath.Join(dir, dictFiles[v])
	finder, err := dawg.Load(path)
	if err != nil {
		return fmt.Errorf("engine: load %s dictionary %q from %s: %w", v, version, path, err)
	}

	r.mu.Lock()
	defer r.mu.Unlock()
	if r.entries[v] == nil {
		r.entries[v] = make(map[string]entry)
	}
	if old, ok := r.entries[v][version]; ok {
		_ = old.finder.Close()
	}
	r.entries[v][version] = entry{finder: finder, solver: scrabble.NewSolver(rs, finder), path: path}
	r.latest[v] = version
	return nil
}

// LoadAvailable loads, under version, every variant whose committed DAWG is
// present in dir, skipping a variant whose file is absent. It backs the admin
// dictionary reload (a version subdirectory may carry only the variants that were
// rebuilt) and OpenWithVersions' boot-time scan. It returns the variants it
// loaded, in catalogue order, or the first load error.
func (r *Registry) LoadAvailable(dir, version string) ([]Variant, error) {
	var loaded []Variant
	for _, v := range Variants() {
		path := filepath.Join(dir, dictFiles[v])
		if _, err := os.Stat(path); err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return loaded, fmt.Errorf("engine: stat %s dictionary %q in %s: %w", v, version, dir, err)
		}
		if err := r.Load(v, version, dir); err != nil {
			return loaded, err
		}
		loaded = append(loaded, v)
	}
	return loaded, nil
}

// Solver returns the solver for the (variant, version) pair, or ErrUnknownVariant
// when the variant is absent and ErrUnknownVersion when only the version is.
func (r *Registry) Solver(v Variant, version string) (*scrabble.Solver, 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.solver, nil
}

// Latest returns the most recently loaded version of variant and its solver, or
// ErrUnknownVariant when none is resident.
func (r *Registry) Latest(v Variant) (string, *scrabble.Solver, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	version, ok := r.latest[v]
	if !ok {
		return "", nil, fmt.Errorf("%w: %s", ErrUnknownVariant, v)
	}
	return version, r.entries[v][version].solver, nil
}

// Versions returns the dictionary versions resident for variant, sorted, or nil
// when none are.
func (r *Registry) Versions(v Variant) []string {
	r.mu.RLock()
	defer r.mu.RUnlock()
	if len(r.entries[v]) == 0 {
		return nil
	}
	versions := make([]string, 0, len(r.entries[v]))
	for ver := range r.entries[v] {
		versions = append(versions, ver)
	}
	sort.Strings(versions)
	return versions
}

// DictBytes returns the raw serialized DAWG for the (variant, version) pair,
// re-read from the file it was loaded from — the same immutable bytes the solver
// holds. It backs the client-side dictionary download for the local move
// preview. It returns ErrUnknownVariant or ErrUnknownVersion when that dictionary
// is not resident, and wraps any read error. The file is read outside the lock.
func (r *Registry) DictBytes(v Variant, version string) ([]byte, error) {
	r.mu.RLock()
	versions, ok := r.entries[v]
	if !ok {
		r.mu.RUnlock()
		return nil, fmt.Errorf("%w: %s", ErrUnknownVariant, v)
	}
	e, ok := versions[version]
	r.mu.RUnlock()
	if !ok {
		return nil, fmt.Errorf("%w: %s/%s", ErrUnknownVersion, v, version)
	}
	data, err := os.ReadFile(e.path)
	if err != nil {
		return nil, fmt.Errorf("engine: read %s/%s dictionary bytes from %s: %w", v, version, e.path, err)
	}
	return data, nil
}

// Lookup reports whether word is present in the (variant, version) dictionary,
// backing the unlimited word-check tool. It returns ErrUnknownVariant or
// ErrUnknownVersion when that dictionary is not resident, and an error when word
// contains a character outside the variant's alphabet. The word is matched as
// given; callers normalise case to the variant's alphabet first.
func (r *Registry) Lookup(v Variant, version, word string) (bool, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	versions, ok := r.entries[v]
	if !ok {
		return false, fmt.Errorf("%w: %s", ErrUnknownVariant, v)
	}
	e, ok := versions[version]
	if !ok {
		return false, fmt.Errorf("%w: %s/%s", ErrUnknownVersion, v, version)
	}
	idx, err := e.finder.IndexOf(word)
	if err != nil {
		return false, fmt.Errorf("engine: lookup %q in %s/%s: %w", word, v, version, err)
	}
	return idx >= 0, nil
}

// Close releases every resident dictionary and empties the registry. It is safe
// to call more than once; the first close error is returned after all finders
// have been closed.
func (r *Registry) Close() error {
	r.mu.Lock()
	defer r.mu.Unlock()
	var firstErr error
	for v, versions := range r.entries {
		for ver, e := range versions {
			if err := e.finder.Close(); err != nil && firstErr == nil {
				firstErr = fmt.Errorf("engine: close %s/%s dictionary: %w", v, ver, err)
			}
		}
	}
	r.entries = make(map[Variant]map[string]entry)
	r.latest = make(map[Variant]string)
	return firstErr
}
