package web

import (
	"encoding/json"
	"time"

	"pete/internal/games/holdem"
	"pete/internal/storage"
)

// Hold'em as a shared table: the seam the runtime drives it through, and the two
// facts about a hand that only the engine can tell the runtime — when the clock
// must next act, and what a finished hand owes the audit trail.
//
// Everything about *playing* poker is still in the engine. This file is the
// translation layer between a holdem.State and the game-agnostic table runtime:
// it decodes the blob, asks the engine to act, and re-derives the deadline and
// the audit from the state that comes back.

// holdemTable is the tableGame for poker. It holds no state — the table's state
// is the blob in game_tables — so a single value in s.tableGames serves every
// felt in the room.
type holdemTable struct{}

func (holdemTable) name() string { return gameHoldem }

// timeout acts for the human whose clock ran out. At a card table the standing
// courtesy is check if you can, fold if you cannot: a walked-away player never
// puts more chips in, and folding keeps the hand moving for everyone still there.
//
// It marks the seat away so the runtime stops waiting a full clock on it next
// time — an absent human auto-folds on sight after this, rather than holding three
// other people hostage for thirty seconds an orbit.
//
// If, on decode, the seat to act is not a waiting human, the scan raced a real
// move that had not yet bumped the version: errNotDue, and the clock steps aside.
func (holdemTable) timeout(state []byte, seats []storage.Seat) (step, []storage.Seat, error) {
	var g holdem.State
	if err := json.Unmarshal(state, &g); err != nil {
		return step{}, nil, err
	}
	if g.Phase != holdem.PhaseBetting {
		return step{}, seats, errNotDue
	}
	seat := g.ToAct
	if seat < 0 || seat >= len(g.Seats) || g.Seats[seat].Bot {
		return step{}, seats, errNotDue
	}

	move := holdem.Move{Kind: holdem.Fold}
	if g.Owed(seat) == 0 {
		move.Kind = holdem.Check
	}
	next, evs, err := holdem.ApplyMove(g, seat, move)
	if err != nil {
		return step{}, nil, err
	}

	changed := markAway(seats, seat)
	st, err := holdemStep(g, next, evs, seats)
	return st, changed, err
}

// stacks reports the chips in front of each seat, index-aligned with the table's
// seat rows. The abandoned-table reaper reads it to know what to send each
// walked-away human home with, without having to understand poker.
func (holdemTable) stacks(state []byte) ([]int64, error) {
	var g holdem.State
	if err := json.Unmarshal(state, &g); err != nil {
		return nil, err
	}
	out := make([]int64, len(g.Seats))
	for i := range g.Seats {
		out[i] = g.Seats[i].Stack
	}
	return out, nil
}

// holdemStep packages a played-out state for the runtime: the blob to persist,
// the deadline the clock must honour next, and the audit of any hand that just
// ended. prev is the state before the move, which is what lets it work out how
// much rake this one hand took.
func holdemStep(prev, next holdem.State, evs []holdem.Event, seats []storage.Seat) (step, error) {
	blob, err := json.Marshal(next)
	if err != nil {
		return step{}, err
	}
	st := step{
		State:    blob,
		Phase:    string(next.Phase),
		HandNo:   int64(next.HandNo),
		Deadline: holdemDeadline(next, seats),
		Audit:    holdemAudit(prev, next, evs, seats),
	}
	return st, nil
}

// holdemDeadline is when the clock must next act, or 0 for never. A clock is only
// ever set on a *present* human whose turn it is: a bot resolves inside the move
// and never waits, and an away human is auto-acted the moment the clock sees them
// rather than waited on. Between hands there is no per-seat clock at all — an
// abandoned table is the reaper's job, not the turn clock's.
func holdemDeadline(g holdem.State, seats []storage.Seat) int64 {
	if g.Phase != holdem.PhaseBetting {
		return 0
	}
	seat := g.ToAct
	if seat < 0 || seat >= len(g.Seats) || g.Seats[seat].Bot {
		return 0
	}
	if seatAway(seats, seat) {
		return 0 // an away human doesn't get waited on; the clock acts next tick
	}
	return time.Now().Unix() + turnSeconds
}

// holdemAudit is the per-hand record of a finished hand — one row per human who
// was in it. Empty until a hand actually ends, which is exactly when the engine
// emits an "end" beat.
//
// The rake is the trap the plan flagged. game_hands.rake is summed into the
// house's income (HouseTake), so a pot's rake must be recorded once and once
// only. It rides on the winner's row alone; every other seat carries zero. And it
// is this hand's rake, not the session's: next.Paid is cumulative, so the hand's
// take is the amount it climbed by since prev — the part of *this* pot that came
// out of a human's winnings.
func holdemAudit(prev, next holdem.State, evs []holdem.Event, seats []storage.Seat) []storage.Hand {
	if !handEnded(evs) {
		return nil
	}
	handRake := next.Paid - prev.Paid

	// The human who won the most is who the rake is attributed to: rake only ever
	// comes out of a pot a human won, so when handRake is positive there is one.
	winner, best := -1, int64(0)
	for i := range next.Seats {
		if next.Seats[i].Bot {
			continue
		}
		if next.Seats[i].Won > best {
			winner, best = i, next.Seats[i].Won
		}
	}

	var audit []storage.Hand
	for i := range next.Seats {
		p := next.Seats[i]
		if p.Bot || i >= len(seats) || seats[i].MatrixUser == "" {
			continue
		}
		if p.Total == 0 && p.Won == 0 {
			continue // dealt out, or never in the hand — nothing to record
		}
		outcome := "lost"
		switch {
		case p.Won > p.Total:
			outcome = "won"
		case p.Won == p.Total:
			outcome = "push"
		}
		rake := int64(0)
		if i == winner {
			rake = handRake
		}
		audit = append(audit, storage.Hand{
			MatrixUser: seats[i].MatrixUser,
			Game:       gameHoldem,
			Bet:        p.Total,
			Payout:     p.Won,
			Rake:       rake,
			Outcome:    outcome,
			Seed1:      next.Seed1,
			Seed2:      next.Seed2,
		})
	}
	return audit
}

// handEnded reports whether a hand finished in this batch of events. endHand is
// the only thing that emits "end", and it emits exactly one, so this is the clean
// signal that Won and Total on the seats are this hand's final numbers.
func handEnded(evs []holdem.Event) bool {
	for _, e := range evs {
		if e.Kind == "end" {
			return true
		}
	}
	return false
}

// ---- seat bookkeeping ------------------------------------------------------

// seatAway reports whether the seat at an index is a human who has walked away.
func seatAway(seats []storage.Seat, seat int) bool {
	return seat >= 0 && seat < len(seats) && seats[seat].Away
}

// markAway returns the one seat row the clock needs to write back: the timed-out
// seat, flagged away, with its last_seen left exactly as it was. Preserving
// last_seen is the whole point — the reaper measures abandonment from when a
// human last acted *for themselves*, and an auto-fold is not that.
func markAway(seats []storage.Seat, seat int) []storage.Seat {
	if seat < 0 || seat >= len(seats) {
		return nil
	}
	s := seats[seat]
	s.Away = true
	return []storage.Seat{s}
}
