package engine

import (
	"fmt"

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

// blankLetter is how a blank tile is written in the decoded, domain-facing API:
// in a rack it is an undesignated blank, and in an exchange it is a blank being
// swapped. A blank placed on the board is a TileRecord with Blank set and Letter
// holding the concrete letter it stands for.
const blankLetter = "?"

// Direction is the orientation of a play as seen by the game domain. It decouples
// the domain from the solver's own Direction type: internal/engine is the only
// backend package that imports scrabble-solver (see docs/ARCHITECTURE.md §5), so
// the engine accepts and returns decoded, solver-free values.
type Direction uint8

const (
	// Horizontal lays a word left to right along a row.
	Horizontal Direction = iota
	// Vertical lays a word top to bottom down a column.
	Vertical
)

// String renders the direction as "H" or "V", the form the move journal and GCG
// export use.
func (d Direction) String() string {
	if d == Vertical {
		return "V"
	}
	return "H"
}

// scrabbleDir maps the domain Direction to the solver's Direction.
func (d Direction) scrabbleDir() scrabble.Direction {
	if d == Vertical {
		return scrabble.Vertical
	}
	return scrabble.Horizontal
}

// fromScrabbleDir maps the solver's Direction to the domain Direction.
func fromScrabbleDir(d scrabble.Direction) Direction {
	if d == scrabble.Vertical {
		return Vertical
	}
	return Horizontal
}

// SubmitPlay validates and applies the current player's play described in decoded
// terms: each TileRecord carries a concrete letter (the letter a blank stands for
// when Blank is set) and a board coordinate. It infers the play's orientation
// from the tiles and the board (resolveDirection), encodes the tiles through the
// ruleset alphabet and delegates to Play, so it returns the same errors
// (ErrTilesNotOnRack, ErrIllegalPlay, ErrGameOver) plus ErrIllegalPlay when a
// letter is outside the variant's alphabet.
func (g *Game) SubmitPlay(tiles []TileRecord) (MoveRecord, error) {
	placements, err := g.placements(tiles)
	if err != nil {
		return MoveRecord{}, err
	}
	return g.Play(g.playDirection(placements), placements)
}

// SubmitPlayDir is SubmitPlay with the orientation supplied rather than inferred.
// It exists for journal replay, which reproduces a committed game exactly from
// the stored "H"/"V" rather than re-deriving it (docs/ARCHITECTURE.md §9.1):
// re-derivation would tie historical reconstruction to the current resolver, so
// replay trusts the recorded direction. Live play uses SubmitPlay.
func (g *Game) SubmitPlayDir(dir Direction, tiles []TileRecord) (MoveRecord, error) {
	placements, err := g.placements(tiles)
	if err != nil {
		return MoveRecord{}, err
	}
	return g.Play(dir.scrabbleDir(), placements)
}

// SubmitExchange swaps the current player's tiles, named in decoded terms: a
// concrete letter per tile, or "?" for a blank. It encodes them and delegates to
// Exchange, returning the same errors plus ErrTilesNotOnRack when a letter is
// outside the variant's alphabet.
func (g *Game) SubmitExchange(tiles []string) (MoveRecord, error) {
	raw, err := g.encodeTiles(tiles)
	if err != nil {
		return MoveRecord{}, err
	}
	return g.Exchange(raw)
}

// EvaluatePlay scores and validates a tentative play without committing it,
// backing the unlimited "what would my next move score, and is it legal?" tool.
// It infers the play's orientation from the tiles and the board and applies the
// game's play options exactly as SubmitPlay does, so under the single-word rule
// perpendicular cross-words are ignored: the preview's legality and score then
// match what submitting the play would yield. It returns the decoded move (placed
// tiles, the words it forms, its orientation and its score) or ErrIllegalPlay when
// the solver rejects it. The board, racks, bag and turn are left untouched.
func (g *Game) EvaluatePlay(tiles []TileRecord) (MoveRecord, error) {
	if g.over {
		return MoveRecord{}, ErrGameOver
	}
	placements, err := g.placements(tiles)
	if err != nil {
		return MoveRecord{}, err
	}
	move, err := g.solver.ValidatePlayOpts(g.board, g.playDirection(placements), placements, g.playOpts())
	if err != nil {
		return MoveRecord{}, fmt.Errorf("%w: %v", ErrIllegalPlay, err)
	}
	move, err = g.noRepeat(move, g.playedWords())
	if err != nil {
		return MoveRecord{}, err
	}
	return g.decodeMove(move), nil
}

// HintView returns the highest-scoring legal play for the current player as a
// decoded MoveRecord and true, or a zero record and false when there is none. It
// is the one-per-game hint's top-1 move in domain-facing form.
func (g *Game) HintView() (MoveRecord, bool) {
	move, ok := g.Hint()
	if !ok {
		return MoveRecord{}, false
	}
	return g.decodeMove(move), true
}

// Candidates returns every legal play for the current player as decoded
// MoveRecords, ranked by descending score (so the first entry equals HintView's
// move). It is empty when the player has no legal play. The robot opponent picks
// from these by margin without importing the solver; each record carries the
// move's score, so a caller can choose by resulting score difference rather than
// always taking the maximum.
func (g *Game) Candidates() []MoveRecord {
	moves := g.GenerateMoves()
	out := make([]MoveRecord, len(moves))
	for i, m := range moves {
		out[i] = g.decodeMove(m)
	}
	return out
}

// Hand returns the player's current rack decoded to concrete letters, with "?"
// for each undesignated blank. The order mirrors the internal hand. It supplies
// the GCG rack field and the per-player game-state view.
func (g *Game) Hand(player int) []string {
	hand := g.hands[player]
	out := make([]string, len(hand))
	for i, t := range hand {
		if t == blankTile {
			out[i] = blankLetter
			continue
		}
		out[i] = g.letter(t)
	}
	return out
}

// placements encodes decoded tiles into solver placements via the ruleset
// alphabet, wrapping a bad letter as ErrIllegalPlay.
func (g *Game) placements(tiles []TileRecord) ([]scrabble.Placement, error) {
	out := make([]scrabble.Placement, len(tiles))
	for i, t := range tiles {
		idx, err := g.rules.Alphabet.Index(t.Letter)
		if err != nil {
			return nil, fmt.Errorf("%w: letter %q at (%d,%d): %v", ErrIllegalPlay, t.Letter, t.Row, t.Col, err)
		}
		out[i] = scrabble.Placement{Row: t.Row, Col: t.Col, Letter: idx, Blank: t.Blank}
	}
	return out, nil
}

// playDirection resolves the orientation for a live play. resolveDirection infers it from
// geometry alone, preferring the longer word when a single tile abuts the board on both
// axes; under the single-word rule that can pick an orientation whose word is not in the
// dictionary while the other orientation's is. So for a single tile under that rule the
// engine tries both orientations through the solver and keeps the higher-scoring legal one
// (horizontal breaks a tie). Multi-tile plays, and every play under the standard rule, keep
// the geometric resolution: a multi-tile play's orientation is fixed by the line its tiles
// share, and under the standard rule every word the play forms must be valid regardless of
// which one is named the main word.
func (g *Game) playDirection(placements []scrabble.Placement) scrabble.Direction {
	geo := resolveDirection(g.board, placements)
	if len(placements) != 1 || g.multipleWords {
		return geo
	}
	best, found, bestScore := geo, false, 0
	for _, dir := range [...]scrabble.Direction{scrabble.Horizontal, scrabble.Vertical} {
		m, err := g.solver.ValidatePlayOpts(g.board, dir, placements, g.playOpts())
		if err != nil {
			continue
		}
		if !found || m.Score > bestScore {
			best, found, bestScore = dir, true, m.Score
		}
	}
	return best
}

// encodeTiles encodes decoded exchange tiles ("?" for a blank, otherwise a
// concrete letter) into the internal byte form, wrapping a bad letter as
// ErrTilesNotOnRack (the caller cannot hold a tile it cannot name).
func (g *Game) encodeTiles(tiles []string) ([]byte, error) {
	raw := make([]byte, len(tiles))
	for i, t := range tiles {
		if t == blankLetter {
			raw[i] = blankTile
			continue
		}
		idx, err := g.rules.Alphabet.Index(t)
		if err != nil {
			return nil, fmt.Errorf("%w: tile %q: %v", ErrTilesNotOnRack, t, err)
		}
		raw[i] = idx
	}
	return raw, nil
}
