// Package engine is the backend's in-process bridge to the scrabble-solver
// library. It catalogues the playable variants, loads versioned dictionaries
// into a registry of solvers, and exposes a pure rules engine (the in-memory
// Game) that drives a match through legal plays, passes, exchanges and
// resignations while detecting the end of the game.
//
// Two invariants shape the package. First, the solver speaks alphabet-index
// bytes that are meaningful only alongside the matching ruleset; every value
// that leaves the engine for persistence or display is decoded to concrete
// characters (see decode.go and docs/ARCHITECTURE.md §9.1), so archived games
// replay independently of any dictionary. Second, the engine owns rules and
// scoring only: turn scheduling, the 24-hour timeout, persistence and transport
// belong to the game domain.
package engine

import (
	"errors"
	"fmt"

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

// Variant identifies a Scrabble variant the backend offers. Each maps to a
// scrabble-solver ruleset and a committed dictionary.
type Variant uint8

const (
	// VariantEnglish is standard English Scrabble (the SOWPODS dictionary).
	VariantEnglish Variant = iota
	// VariantRussianScrabble is Russian Scrabble.
	VariantRussianScrabble
	// VariantErudit is the Russian "Эрудит" variant.
	VariantErudit
)

// String returns the variant's stable identifier, used in logs and as a metadata
// label on persisted games.
func (v Variant) String() string {
	switch v {
	case VariantEnglish:
		return "scrabble_en"
	case VariantRussianScrabble:
		return "scrabble_ru"
	case VariantErudit:
		return "erudit_ru"
	}
	return "unknown"
}

// Language returns the variant's interface/bot language tag: "en" for English Scrabble, "ru" for
// the Russian variants (Russian Scrabble and Erudite). It routes a game's out-of-app push to the
// matching per-language Telegram bot — by the game, not the recipient's last-login bot.
func (v Variant) Language() string {
	if v == VariantEnglish {
		return "en"
	}
	return "ru"
}

// ruleset returns the scrabble-solver ruleset backing the variant and true, or
// (nil, false) for an unrecognised variant.
func (v Variant) ruleset() (*rules.Ruleset, bool) {
	switch v {
	case VariantEnglish:
		return rules.English(), true
	case VariantRussianScrabble:
		return rules.RussianScrabble(), true
	case VariantErudit:
		return rules.Erudit(), true
	}
	return nil, false
}

// Variants returns the variants the backend offers, in catalogue order.
func Variants() []Variant {
	return []Variant{VariantEnglish, VariantRussianScrabble, VariantErudit}
}

// ParseVariant maps a stable label produced by Variant.String back to its
// Variant, or returns ErrUnknownVariant. It is the inverse the game domain uses
// to read a persisted variant.
func ParseVariant(s string) (Variant, error) {
	for _, v := range Variants() {
		if v.String() == s {
			return v, nil
		}
	}
	return 0, fmt.Errorf("%w: %q", ErrUnknownVariant, s)
}

// Ruleset returns the scrabble-solver ruleset for variant. It needs no
// dictionary, so it supports dictionary-independent board replay (see
// ReplayBoard) from a finished game's variant metadata alone.
func Ruleset(v Variant) (*rules.Ruleset, error) {
	rs, ok := v.ruleset()
	if !ok {
		return nil, fmt.Errorf("%w: %d", ErrUnknownVariant, v)
	}
	return rs, nil
}

// Sentinel errors returned across the engine. Callers match them with
// errors.Is; the wrapped detail carries the offending value.
var (
	// ErrUnknownVariant is returned for a variant the engine does not recognise.
	ErrUnknownVariant = errors.New("engine: unknown variant")
	// ErrUnknownVersion is returned when no dictionary is registered for a
	// (variant, version) pair.
	ErrUnknownVersion = errors.New("engine: unknown dictionary version")
	// ErrUnknownDropoutTiles is returned by ParseDropoutTiles for a label that is
	// neither "remove" nor "return".
	ErrUnknownDropoutTiles = errors.New("engine: unknown drop-out tile disposition")
	// ErrIllegalPlay wraps a solver validation failure: off-board geometry, a
	// word absent from the dictionary, or a play that does not connect.
	ErrIllegalPlay = errors.New("engine: illegal play")
	// ErrTilesNotOnRack is returned when a play or exchange references tiles the
	// acting player does not hold.
	ErrTilesNotOnRack = errors.New("engine: tiles not on the player's rack")
	// ErrNotEnoughTilesToExchange is returned when an exchange is attempted while
	// the bag holds fewer tiles than a full rack.
	ErrNotEnoughTilesToExchange = errors.New("engine: not enough tiles in the bag to exchange")
	// ErrNothingToExchange is returned for an exchange of zero tiles.
	ErrNothingToExchange = errors.New("engine: exchange requires at least one tile")
	// ErrGameOver is returned when a transition is attempted on a finished game.
	ErrGameOver = errors.New("engine: game is over")
)
