package web

import (
	"errors"

	"pete/internal/storage"
)

// errNotDue is a timeout that turned out to have nothing to do. The clock scanned
// a table as expired, took the lock, and found — on decoding the state — that the
// seat to act is not in fact a waiting human: a real move landed in the instant
// between the scan and the lock and had not yet bumped the version the clock
// checked. It is not an error, it is the race resolving the right way, so the
// clock swallows it silently rather than logging.
var errNotDue = errors.New("games: nothing to time out")

// The table runtime: the game-agnostic half of a shared table.
//
// A shared table has two writers where a solo game had one — an HTTP move, and a
// turn clock acting for whoever walked away — and the whole job of this layer is
// to let those two coexist without either trusting the other. The rule that makes
// that work is the database's version column: every write is conditional on the
// version its writer read, so the two can race freely and exactly one wins.
//
// Everything specific to a game — how a move advances it, whose turn it is now,
// what a settled hand pays — lives behind the tableGame interface. The clock, the
// lock discipline and the SSE publish do not know whether they are driving poker
// or UNO, and that is what lets Phase B ship before any engine is multiway.

// turnSeconds is how long a human has to act before the clock acts for them. Long
// enough to read the table and think, short enough that a walked-away player does
// not hold three others hostage.
const turnSeconds = 30

// bootGrace is how far the turn clock shoves every live deadline out on boot. A
// deploy takes the in-memory clock with it, so without this the first tick after
// a restart would find every deadline in the casino already past and auto-act the
// whole room at once.
const bootGrace = 30

// step is what a game hands back after a move or a timeout: the new state, ready
// to persist, plus everything the runtime needs to settle and to schedule.
//
// The chips are inside State — a hand ending moves the pot within the blob and
// credits nobody — so there is no payout field. What comes out is the state, the
// audit of any hand that just ended, and the clock's next deadline.
type step struct {
	// State is the engine's whole state, marshalled, ready for the table blob.
	State []byte
	// Phase is lifted out of the state so the lobby can read it without decoding.
	Phase string
	// HandNo is the hand this state is on. It advances when a new hand is dealt,
	// and it is the audit key now that a seed no longer reproduces a shared hand.
	HandNo int64
	// Deadline is when the clock must next act, or 0 for none. It is nonzero only
	// when the turn has landed on a *present* human: a bot resolves inside the move
	// and an away human is auto-acted on sight, so neither is ever waited for.
	Deadline int64
	// Audit is the per-seat record of a hand that ended in this step. Empty if no
	// hand settled.
	Audit []storage.Hand
	// Events is the script the felt plays back — the same shape every solo game
	// already returns. It is what the SSE frame and the acting player both animate.
	Events any
}

// tableGame is everything the runtime needs from an engine to run it at a shared
// table. Each multiplayer game implements it; the clock and the handlers hold it
// as an interface so they stay game-agnostic.
type tableGame interface {
	// name is the storage key and the lobby label: "holdem", "uno", "blackjack".
	name() string

	// timeout acts for the human whose clock has expired — check if the rules
	// allow it, fold otherwise — and advances the table as far as the next
	// decision, exactly as a real move would. seats is the current roster, so the
	// engine can mark the timed-out player away.
	//
	// It returns ErrNotDue (via a nil step, see runClockTable) if, on decode, the
	// seat to act is not in fact a waiting human — which happens when a real move
	// landed in the same instant the clock fired and the version had not yet been
	// bumped when the clock scanned.
	timeout(state []byte, seats []storage.Seat) (step, []storage.Seat, error)

	// stacks reports the chips in front of each seat, index-aligned with the
	// table's seat rows, so the abandoned-table reaper can cash out a walked-away
	// player without knowing how their game is played.
	stacks(state []byte) ([]int64, error)
}
