package web

import (
	"encoding/json"
	"time"

	"pete/internal/games/uno"
	"pete/internal/storage"
)

// UNO as a shared table: the seam the runtime drives it through, and the two
// facts about a hand 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* UNO is still in the engine. This file is the
// translation layer between a uno.State and the game-agnostic table runtime.

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

func (unoTable) name() string { return gameUno }

// timeout plays a passive move for the human whose clock ran out and marks them
// away, so the runtime auto-acts them on sight after this rather than waiting a
// full clock every orbit. The move is the gentlest one that still advances the
// turn: give in to a stack, pass a drawn card (or play it where the rules force
// it), otherwise play a legal card if there is one, or draw.
//
// 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 (unoTable) timeout(state []byte, seats []storage.Seat) (step, []storage.Seat, error) {
	var g uno.State
	if err := json.Unmarshal(state, &g); err != nil {
		return step{}, nil, err
	}
	if !unoPlaying(g) {
		return step{}, seats, errNotDue
	}
	seat := g.Turn
	if seat < 0 || seat >= len(g.Seats) || g.Seats[seat].Bot {
		return step{}, seats, errNotDue
	}

	move := unoIdleMove(g, seat)
	next, evs, err := uno.ApplyMove(g, seat, move)
	if err != nil {
		return step{}, nil, err
	}

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

// unoIdleMove is the move the clock makes for a walked-away seat: the most passive
// legal one that still hands the turn on.
func unoIdleMove(g uno.State, seat int) uno.Move {
	switch g.Phase {
	case uno.PhaseStack:
		return uno.Move{Kind: uno.MoveTake}
	case uno.PhaseDrawn:
		if g.Tier.NoMercy {
			// No Mercy makes you play the card you drew; it is the last in the hand.
			return uno.Move{Kind: uno.MovePlay, Index: len(g.Hands[seat]) - 1, Color: uno.Red}
		}
		return uno.Move{Kind: uno.MovePass}
	default:
		if p := g.Playable(seat); len(p) > 0 {
			return uno.Move{Kind: uno.MovePlay, Index: p[0], Color: uno.Red}
		}
		return uno.Move{Kind: uno.MoveDraw}
	}
}

// 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 human.
func (unoTable) stacks(state []byte) ([]int64, error) {
	var g uno.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
}

// unoStep 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.
func unoStep(next uno.State, evs []uno.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: unoDeadline(next, seats),
		Audit:    unoAudit(next, evs, seats),
	}
	return st, nil
}

// unoPlaying reports whether a hand is in progress — the only phase a turn clock
// has anything to do in.
func unoPlaying(g uno.State) bool {
	switch g.Phase {
	case uno.PhasePlay, uno.PhaseDrawn, uno.PhaseStack:
		return true
	}
	return false
}

// unoDeadline is when the clock must next act, or 0 for never. A clock is only set
// on a *present* human whose turn it is: a bot resolves inside the move, an away
// human is auto-acted on sight, and between hands there is no per-seat clock —
// dealing the next hand is a seated player's call, and an abandoned table is the
// reaper's job.
func unoDeadline(g uno.State, seats []storage.Seat) int64 {
	if !unoPlaying(g) {
		return 0
	}
	seat := g.Turn
	if seat < 0 || seat >= len(g.Seats) || g.Seats[seat].Bot {
		return 0
	}
	if seatAway(seats, seat) {
		return 0
	}
	return time.Now().Unix() + turnSeconds
}

// unoAudit 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 a settle beat is
// emitted.
//
// The rake rides on the winner's row alone, and every other seat carries zero, so
// game_hands.rake (which HouseTake sums) records a pot's rake once and once only.
// A seat's ante is its bet; a refunded tie pays each seat its ante straight back.
func unoAudit(g uno.State, evs []uno.Event, seats []storage.Seat) []storage.Hand {
	if !unoHandEnded(evs) {
		return nil
	}
	var audit []storage.Hand
	for i := range g.Seats {
		p := g.Seats[i]
		if p.Bot || i >= len(seats) || seats[i].MatrixUser == "" {
			continue
		}
		if p.Ante == 0 {
			continue // sat this hand out — nothing to record
		}
		outcome, payout, rake := "lost", int64(0), int64(0)
		switch {
		case i == g.Winner:
			outcome, payout, rake = "won", p.Won, g.Rake
		case g.Outcome == uno.OutcomeTie:
			outcome, payout = "push", p.Ante // the ante came straight back
		}
		audit = append(audit, storage.Hand{
			MatrixUser: seats[i].MatrixUser,
			Game:       gameUno,
			Bet:        p.Ante,
			Payout:     payout,
			Rake:       rake,
			Outcome:    outcome,
			Seed1:      g.Seed1,
			Seed2:      g.Seed2,
		})
	}
	return audit
}

// unoHandEnded reports whether a hand finished in this batch of events. A settle
// beat is emitted exactly once, when a hand ends, so it is the clean signal that
// the seats' Ante/Won are this hand's final numbers.
func unoHandEnded(evs []uno.Event) bool {
	for _, e := range evs {
		if e.Kind == uno.EvSettle {
			return true
		}
	}
	return false
}
