// Package moves turns a game's public history and the caller's private rack into a
// legal turn, by reconstructing the board and running the embedded scrabble-solver
// locally (the edge protocol carries no board — the client replays history). It
// picks a mid-ranked move so games progress realistically rather than optimally.
package moves

import (
	"fmt"
	"math/rand"
	"path/filepath"

	"gitea.iliadenisov.ru/developer/scrabble-solver/board"
	"gitea.iliadenisov.ru/developer/scrabble-solver/rack"
	"gitea.iliadenisov.ru/developer/scrabble-solver/rules"
	"gitea.iliadenisov.ru/developer/scrabble-solver/scrabble"
	dawg "github.com/iliadenisov/dafsa"

	"scrabble/loadtest/internal/edge"
)

// blankIndex is the rack/exchange sentinel for a blank tile on the wire.
const blankIndex = 255

// variantSpec maps an edge variant label to its ruleset constructor and committed
// DAWG filename (using descriptive names).
type variantSpec struct {
	ruleset func() *rules.Ruleset
	dawg    string
}

var specs = map[string]variantSpec{
	"scrabble_en": {rules.English, "en_sowpods.dawg"},
	"scrabble_ru": {rules.RussianScrabble, "ru_scrabble.dawg"},
	"erudit_ru":   {rules.Erudit, "ru_erudit.dawg"},
}

// Variants returns the edge variant labels the harness drives, in catalogue order.
func Variants() []string { return []string{"scrabble_en", "scrabble_ru", "erudit_ru"} }

// engine is one loaded variant: its ruleset and a solver over its DAWG.
type engine struct {
	rs     *rules.Ruleset
	finder dawg.Finder
	solver *scrabble.Solver
}

// Registry holds a solver per variant, built from the committed DAWGs in dir. It is
// safe for concurrent use: every Pick builds its own board and rack, and the solver
// holds only read-only state (the same way the backend shares one solver per variant
// across concurrent games).
type Registry struct {
	engines map[string]*engine
}

// Open loads every variant's DAWG from dir and builds a solver over each. dir holds
// the committed dawg files (the sibling scrabble-solver checkout's dawg/, or the
// dictionary release artifact).
func Open(dir string) (*Registry, error) {
	r := &Registry{engines: make(map[string]*engine)}
	for label, spec := range specs {
		rs := spec.ruleset()
		finder, err := dawg.Load(filepath.Join(dir, spec.dawg))
		if err != nil {
			r.Close()
			return nil, fmt.Errorf("moves: load %s dawg %s from %s: %w", label, spec.dawg, dir, err)
		}
		r.engines[label] = &engine{rs: rs, finder: finder, solver: scrabble.NewSolver(rs, finder)}
	}
	return r, nil
}

// Close releases every loaded DAWG.
func (r *Registry) Close() {
	for _, e := range r.engines {
		if e.finder != nil {
			_ = e.finder.Close()
		}
	}
}

// Action is a chosen turn. Kind is "play", "exchange" or "pass". A play carries Tiles
// (the backend infers the orientation); an exchange carries Exchange (rack indices to swap).
type Action struct {
	Kind     string
	Tiles    []edge.PlayTile
	Exchange []byte
}

// Pick reconstructs the board for variant from history, builds the rack from the
// alphabet-index rack, generates the legal plays and returns a mid-ranked one. With
// no legal play it exchanges (when the bag holds a full rack) or passes. rng makes
// the choice deterministic per caller; pass each virtual player its own *rand.Rand
// (rand.Rand is not safe for concurrent use).
func (r *Registry) Pick(variant string, history []edge.Move, rackIdx []byte, bagLen int, rng *rand.Rand) (Action, error) {
	e, ok := r.engines[variant]
	if !ok {
		return Action{}, fmt.Errorf("moves: unknown variant %q", variant)
	}
	b, err := replayBoard(e.rs, history)
	if err != nil {
		return Action{}, err
	}
	legal := e.solver.GenerateMoves(b, buildRack(e.rs, rackIdx), scrabble.Both)
	if len(legal) == 0 {
		return noPlay(rackIdx, bagLen >= e.rs.RackSize), nil
	}
	m := midRanked(legal, rng)
	return Action{Kind: "play", Tiles: toPlayTiles(m.Tiles)}, nil
}

// toPlayTiles maps the solver's newly-placed tiles to the edge submit-play tiles
// (addressed by alphabet index, carrying the blank flag).
func toPlayTiles(placements []scrabble.Placement) []edge.PlayTile {
	tiles := make([]edge.PlayTile, len(placements))
	for i, p := range placements {
		tiles[i] = edge.PlayTile{Row: p.Row, Col: p.Col, Letter: p.Letter, Blank: p.Blank}
	}
	return tiles
}

// replayBoard mirrors backend engine.ReplayBoard using only the solver's public API:
// each play record's letters are re-indexed through the alphabet and applied to an
// empty board. Non-play records are ignored.
func replayBoard(rs *rules.Ruleset, history []edge.Move) (*board.Board, error) {
	b := board.New(rs.Rows, rs.Cols)
	for _, rec := range history {
		if rec.Action != "play" {
			continue
		}
		ps := make([]scrabble.Placement, len(rec.Tiles))
		for i, t := range rec.Tiles {
			idx, err := rs.Alphabet.Index(t.Letter)
			if err != nil {
				return nil, fmt.Errorf("moves: replay letter %q at (%d,%d): %w", t.Letter, t.Row, t.Col, err)
			}
			ps[i] = scrabble.Placement{Row: t.Row, Col: t.Col, Letter: idx, Blank: t.Blank}
		}
		scrabble.Apply(b, scrabble.Move{Tiles: ps})
	}
	return b, nil
}

// buildRack turns the alphabet-index rack (255 a blank) into a solver Rack.
func buildRack(rs *rules.Ruleset, rackIdx []byte) rack.Rack {
	rk := rack.New(rs.Alphabet.Size())
	for _, idx := range rackIdx {
		if idx == blankIndex {
			rk.AddBlank()
		} else {
			rk.Add(idx)
		}
	}
	return rk
}

// midRanked returns a move from the middle third of the score-ranked list
// (GenerateMoves returns highest-first), spreading the pick within that band with
// rng. A tiny list yields its lowest-scoring move.
func midRanked(moves []scrabble.Move, rng *rand.Rand) scrabble.Move {
	n := len(moves)
	if n <= 2 {
		return moves[n-1]
	}
	lo, hi := n/3, 2*n/3
	if hi <= lo {
		hi = lo + 1
	}
	return moves[lo+rng.Intn(hi-lo)]
}

// noPlay chooses an exchange (when the bag can refill a full rack) or a pass.
func noPlay(rackIdx []byte, canExchange bool) Action {
	if canExchange && len(rackIdx) > 0 {
		return Action{Kind: "exchange", Exchange: append([]byte(nil), rackIdx...)}
	}
	return Action{Kind: "pass"}
}
