package game

import (
	"errors"
	"time"

	"github.com/google/uuid"

	"scrabble/backend/internal/engine"
	"scrabble/backend/internal/gamelimits"
)

// Status values persisted in the games.status column.
const (
	StatusActive   = "active"
	StatusFinished = "finished"
	// StatusOpen is an auto-match game whose starter has already entered it but
	// which is still waiting for an opponent: the opponent seat holds no account
	// yet. The starter may move on their turn; it becomes StatusActive when a human
	// or a robot joins (see OpenGame, Service.OpenOrJoin and Service.AttachRobot).
	StatusOpen = "open"
)

// Complaint lifecycle values. A complaint is filed StatusComplaintOpen
// and closed StatusComplaintResolved by the admin review queue with a
// Disposition. The CHECK constraints live in the baseline migration.
const (
	StatusComplaintOpen     = "open"
	StatusComplaintResolved = "resolved"
)

// Complaint dispositions chosen at resolution. DispositionReject keeps the
// dictionary as-is; DispositionAcceptAdd / DispositionAcceptRemove mark the word
// for addition to / removal from the variant's wordlist and feed the offline
// dictionary-rebuild pipeline (see DictionaryChange).
const (
	DispositionReject       = "reject"
	DispositionAcceptAdd    = "accept_add"
	DispositionAcceptRemove = "accept_remove"
)

// Sentinel errors returned by the service. Engine errors (engine.ErrIllegalPlay,
// engine.ErrTilesNotOnRack, engine.ErrGameOver, …) propagate unwrapped from the
// transitions, so callers may match either taxonomy with errors.Is.
var (
	// ErrNotFound is returned when no game matches the lookup.
	ErrNotFound = errors.New("game: not found")
	// ErrNotYourTurn is returned when an account acts out of turn.
	ErrNotYourTurn = errors.New("game: not the player's turn")
	// ErrFinished is returned when a transition is attempted on a finished game.
	ErrFinished = errors.New("game: game is finished")
	// ErrNoOpponentYet is returned when an action that needs a present opponent
	// (resign, chat, nudge) is attempted on an auto-match game still waiting for one
	// (StatusOpen).
	ErrNoOpponentYet = errors.New("game: no opponent has joined yet")
	// ErrGameActive is returned when an operation allowed only on a finished game
	// (such as a GCG export) is attempted while the game is still active.
	ErrGameActive = errors.New("game: game is still active")
	// ErrNotAPlayer is returned when an account is not seated in the game.
	ErrNotAPlayer = errors.New("game: account is not a player in this game")
	// ErrInvalidConfig is returned when CreateParams are not acceptable.
	ErrInvalidConfig = errors.New("game: invalid game configuration")
	// ErrHintsDisabled is returned when hints are switched off for the game.
	ErrHintsDisabled = errors.New("game: hints are disabled for this game")
	// ErrNoHintsLeft is returned when the player's allowance and wallet are spent.
	ErrNoHintsLeft = errors.New("game: no hints remaining")
	// ErrNoHintAvailable is returned when the player has no legal play to reveal.
	ErrNoHintAvailable = errors.New("game: no legal move to suggest")
	// ErrHintLocked is returned when a vs_ai game's idle hint is still gated (the player has not yet
	// been stuck the idle window since the robot's last move). The client normally pre-gates from
	// StateView.HintUnlockLeftSeconds, so this is the authoritative server-clock backstop.
	ErrHintLocked = errors.New("game: hint is not available yet")
	// ErrGameLimitReached is returned when an account already holds MaxActiveQuickGames
	// simultaneous quick games and tries to create another game (quick or by invitation).
	ErrGameLimitReached = errors.New("game: simultaneous game limit reached")
)

// AllowedTurnTimeouts is the set of per-game move clocks a game may be created
// with, matching the values offered at creation. DefaultTurnTimeout is used when
// none is requested.
var AllowedTurnTimeouts = []time.Duration{
	5 * time.Minute, 10 * time.Minute, 15 * time.Minute, 30 * time.Minute,
	1 * time.Hour, 2 * time.Hour, 3 * time.Hour, 6 * time.Hour, 12 * time.Hour, 24 * time.Hour,
}

// DefaultTurnTimeout is the move clock applied when CreateParams.TurnTimeout is
// zero (the owner's default: a full day).
const DefaultTurnTimeout = 24 * time.Hour

// AIInactivityTimeout is the move clock for an honest-AI game (CreateParams.VsAI).
// These games have no short per-move timeout; instead the player loses a game they
// abandon after seven days of inactivity. Because the robot moves immediately, only
// the human is ever on the clock, so the per-turn timeout doubles as the abandon
// rule (the sweeper resigns the overdue seat). It is set programmatically and is not
// one of AllowedTurnTimeouts (never offered in the creation UI).
const AIInactivityTimeout = 7 * 24 * time.Hour

// aiPlayerName labels the robot seat in an honest-AI game's GCG export, so a downloaded
// game file shows a clean "AI" rather than the robot's human-like pool name (the in-app
// UI shows 🤖 from the game's vs_ai flag).
const aiPlayerName = "AI"

// CreateParams describes a new game. Seats lists the seated accounts in turn
// order (seat 0 moves first); lobby/matchmaking assembles it.
type CreateParams struct {
	Variant        engine.Variant
	Seats          []uuid.UUID
	TurnTimeout    time.Duration // one of AllowedTurnTimeouts; zero → DefaultTurnTimeout
	HintsAllowed   bool
	HintsPerPlayer int                 // starting per-seat hint allowance
	DropoutTiles   engine.DropoutTiles // disposition of a dropped-out seat's tiles (3+ players); zero → remove
	Seed           int64               // zero → a random seed is chosen
	// MultipleWordsPerTurn true selects standard Scrabble; false the single-word rule —
	// only the main word is validated and scored. Russian games default to false.
	MultipleWordsPerTurn bool
	// VsAI creates an honest-AI game against a pooled robot (see games.vs_ai): the
	// robot is seated at once, the move clock is AIInactivityTimeout, and chat/nudge
	// and finish-time statistics are suppressed. Set by the lobby's AI-match path.
	VsAI bool
	// Kind tags the game's origin (games.game_kind) for the active-game limits: vs_ai / random /
	// friends. The lobby sets it; a zero (unknown) kind is never gated. The active-game limit itself
	// is enforced at the new-game handler (Server.ensureUnderGameLimit), not here.
	Kind gamelimits.Kind
}

// Game is the persisted state of a match: the games row joined with its seats.
type Game struct {
	ID             uuid.UUID
	Variant        engine.Variant
	DictVersion    string
	Status         string
	Players        int
	ToMove         int
	TurnStartedAt  time.Time
	TurnTimeout    time.Duration
	HintsAllowed   bool
	HintsPerPlayer int
	DropoutTiles   engine.DropoutTiles
	MoveCount      int
	EndReason      string // "" while active
	Seats          []Seat
	CreatedAt      time.Time
	UpdatedAt      time.Time
	FinishedAt     *time.Time
	// MultipleWordsPerTurn true selects standard Scrabble; false the single-word rule.
	MultipleWordsPerTurn bool
	// NoRepeatWords true forbids laying a word that is already on the board (the "Эрудит" rule,
	// see engine/norepeat.go). It is pinned at creation from the variant rather than derived on
	// read, so a game started before the rule existed keeps replaying and scoring unchanged.
	NoRepeatWords bool
	// VsAI is true for an honest-AI game (games.vs_ai): the opponent is a robot the
	// player knowingly chose, shown as 🤖, with chat/nudge disabled and no statistics.
	VsAI bool
	// Kind is the game's origin tag (games.game_kind) for the active-game limits: vs_ai / random /
	// friends, or unknown for an untagged game. Read-only projection; set once on creation.
	Kind gamelimits.Kind
}

// Seat is one player's standing in a game.
type Seat struct {
	Seat      int
	AccountID uuid.UUID
	Score     int
	// HintsUsed is the total hints the seat used this game — both the free per-game allowance
	// and the wallet-charged ones (the first HintsPerPlayer being the allowance).
	HintsUsed int
	IsWinner  bool
	// DisplayName is the seat's display-name snapshot, captured when the seat was taken
	// (the human's then-current name, or a disguised robot's per-game name). Empty for a
	// still-empty open seat or a pre-snapshot legacy row, where readers fall back to the
	// account's current display name (docs/ARCHITECTURE.md §7).
	DisplayName string
}

// OpenGame identifies an auto-match game waiting for an opponent whose robot
// deadline has passed: the game the matchmaking reaper fills and the variant that
// selects the substitute robot.
type OpenGame struct {
	ID      uuid.UUID
	Variant engine.Variant
}

// seatOf returns the seat index of accountID and true, or (0, false) when the
// account is not seated.
func (g Game) seatOf(accountID uuid.UUID) (int, bool) {
	for _, s := range g.Seats {
		if s.AccountID == accountID {
			return s.Seat, true
		}
	}
	return 0, false
}

// seatedIn reports whether accountID holds a seat in seats. It backs the read-side
// membership check against the cached, immutable seat list, so a hot read can skip
// loading the game from the store.
func seatedIn(seats []Seat, accountID uuid.UUID) bool {
	for _, s := range seats {
		if s.AccountID == accountID {
			return true
		}
	}
	return false
}

// MoveResult is the outcome of a committed transition: the decoded move and the
// post-move game, plus the actor's own refilled rack and the bag size after the draw
// (Rack/BagLen), so the mover renders the next state from the response without a
// follow-up game.state.
type MoveResult struct {
	Move   engine.MoveRecord
	Game   Game
	Rack   []string
	BagLen int
}

// HintResult is a revealed hint with the per-seat allowance remaining (HintsRemaining) and the
// purchasable hint wallet after spending one (WalletBalance, from payments). The client adopts
// WalletBalance into the profile so the badge stays live across games (lib/hints).
type HintResult struct {
	Move           engine.MoveRecord
	HintsRemaining int
	WalletBalance  int
}

// EvalResult previews a tentative play without committing it. Dir is the
// orientation the engine inferred for the play ("H"/"V"), empty when the play is
// illegal; Words lists the words it would form, the main word first.
type EvalResult struct {
	Valid bool
	Score int
	Words []string
	Dir   string
}

// StateView is a player's view of a game: the shared game plus their private
// rack and remaining hint budget. The board for live rendering is reconstructed
// by the client from History; it is added to the gateway surface in a later
// stage.
type StateView struct {
	Game           Game
	Seat           int
	Rack           []string
	BagLen         int
	HintsRemaining int
	// HintUnlockLeftSeconds is, for a vs_ai game on the requesting player's turn, the seconds left
	// until the idle hint unlocks (the robot's last move plus the idle window, from the server clock);
	// 0 for the human's first move, when it is not their turn, or a non-vs_ai game. The vs_ai hint is
	// unlimited and wallet-free; this idle gate replaces the allowance/wallet for it.
	HintUnlockLeftSeconds int
}

// HistoryMove is one decoded journal row, independent of any dictionary.
type HistoryMove struct {
	Seq          int
	Seat         int
	Action       string
	Score        int
	RunningTotal int
	Dir          string // play only: "H"/"V"
	MainRow      int    // play only: main word's first-letter row
	MainCol      int    // play only: main word's first-letter column
	Tiles        []engine.TileRecord
	Words        []string
	Exchanged    []string // exchange only
	Rack         []string // the acting player's rack before the move
}

// HistoryView is a game's full, dictionary-independent move history.
type HistoryView struct {
	Game  Game
	Moves []HistoryMove
}

// RobotTurn is the robot driver's view of one active game seating a robot: the
// seat the robot holds, whose turn it currently is, when that turn started, the
// move index and the bag seed. Seed is backend-internal state (never exposed in
// the public Game view); the robot derives its deterministic per-game behaviour
// from it, so the scheduler stays stateless and restart-safe.
type RobotTurn struct {
	GameID        uuid.UUID
	RobotID       uuid.UUID
	RobotSeat     int
	ToMove        int
	TurnStartedAt time.Time
	MoveCount     int
	Seed          int64
	// VsAI is true when the game is an honest-AI game: the driver then makes the
	// robot move immediately, with no sleep window and no proactive nudge.
	VsAI bool
	// EndgamePass is true when the two most recent committed moves are both passes, so
	// the board and racks are frozen and the robot is bound to pass again. The driver
	// then shortens the robot's think time (see robot.endgamePassDelay) so a decided
	// game is not dragged out. It is false until at least two moves exist.
	EndgamePass bool
	// OppLastMove is the human's think time on the most recent move — the gap between
	// the last two journal entries — used to scale the shortened endgame think time. It
	// is meaningful only when EndgamePass is true (zero otherwise).
	OppLastMove time.Duration
}

// Complaint is a word-check complaint in the admin review queue. It is filed
// against a game's pinned (Variant, DictVersion) with the lookup result at filing
// time (WasValid); the resolution fields stay empty until an operator resolves it.
type Complaint struct {
	ID            uuid.UUID
	ComplainantID uuid.UUID
	GameID        uuid.UUID
	Variant       engine.Variant
	DictVersion   string
	Word          string
	WasValid      bool
	Note          string
	Status        string
	CreatedAt     time.Time

	// Resolution fields, set when Status == StatusComplaintResolved.
	Disposition      string     // "" while open; otherwise a Disposition* value
	ResolutionNote   string     // operator note recorded at resolution
	ResolvedAt       *time.Time // nil while open
	AppliedInVersion string     // dict version an accepted change was folded into ("" = pending)
}

// DictionaryChange is the wordlist edit implied by one resolved, accepted
// complaint: Add reports whether Word should be added (DispositionAcceptAdd) or
// removed (DispositionAcceptRemove) for Variant. The admin console lists the
// pending changes as the input to the offline DAWG rebuild; once a rebuilt
// dictionary version is installed they are marked applied.
type DictionaryChange struct {
	ComplaintID uuid.UUID
	Variant     engine.Variant
	Word        string
	Add         bool
	ResolvedAt  time.Time
	Note        string
}
