// Package blackjack is a pure blackjack engine.
//
// It knows nothing about HTTP, sockets, timers, euros or players' names. You
// hand it a state and a move, it hands you back a new state and the list of
// things that just happened. Everything else — who is sitting there, what their
// chips are, when their clock runs out — belongs to the shell in internal/games/table.
//
// That seam is the one thing gogobee's blackjack never had: there, the engine
// *was* the message sender, so an "error" meant a Matrix send had failed rather
// than that a player had tried something illegal. Here an error means exactly
// one thing: the move was not legal in this state.
//
// The state is a plain value. It serializes, so a hand survives a redeploy, and
// it replays, so a disputed hand can be dealt again from its seed.
package blackjack

import (
	"errors"
	"math"
	"math/rand/v2"

	"pete/internal/games/cards"
)

// Errors an illegal move can produce. Callers can match on these to tell a
// player "not now" rather than "something broke".
var (
	ErrHandOver      = errors.New("blackjack: the hand is already over")
	ErrNotYourTurn   = errors.New("blackjack: it is not the player's turn to act")
	ErrUnknownMove   = errors.New("blackjack: unknown move")
	ErrCantDouble    = errors.New("blackjack: double is only allowed on the opening two cards")
	ErrDeckExhausted = errors.New("blackjack: the shoe is empty")
	ErrBadBet        = errors.New("blackjack: bet must be positive")
)

// Phase is whose turn it is.
type Phase string

const (
	PhasePlayer Phase = "player" // the player is acting
	PhaseDealer Phase = "dealer" // transient: the dealer is drawing out
	PhaseDone   Phase = "done"   // settled, Outcome and Payout are final
)

// Outcome is how a finished hand finished, from the player's point of view.
type Outcome string

const (
	OutcomeNone       Outcome = ""
	OutcomeBlackjack  Outcome = "blackjack" // natural 21, paid 3:2
	OutcomeWin        Outcome = "win"
	OutcomeLose       Outcome = "lose"
	OutcomePush       Outcome = "push" // tie, stake returned
	OutcomeBust       Outcome = "bust" // player went over 21
	OutcomeDealerBust Outcome = "dealer_bust"
)

// Won reports whether this outcome pays the player more than their stake back.
func (o Outcome) Won() bool {
	return o == OutcomeWin || o == OutcomeBlackjack || o == OutcomeDealerBust
}

// Rules are the table's terms. They're part of the state rather than a global,
// so a hand always settles under the rules it was dealt under — even if the
// house changes them mid-session.
type Rules struct {
	Decks            int     `json:"decks"`              // shoe size
	BlackjackPays    float64 `json:"blackjack_pays"`     // 1.5 = the honest 3:2
	DealerHitsSoft17 bool    `json:"dealer_hits_soft17"` // gogobee's dealer does
	RakePct          float64 `json:"rake_pct"`           // house cut, taken from winnings only
}

// DefaultRules match the blackjack gogobee has been dealing in Matrix for years:
// six decks, 3:2 on a natural, dealer hits soft 17. The rake is the one new term
// — see Settle for exactly what it touches.
func DefaultRules() Rules {
	return Rules{Decks: 6, BlackjackPays: 1.5, DealerHitsSoft17: true, RakePct: 0.05}
}

// State is one hand of heads-up blackjack: one player, one dealer. Splitting
// isn't in v1, so there's exactly one player hand.
type State struct {
	Rules  Rules        `json:"rules"`
	Deck   cards.Deck   `json:"deck"` // the shoe, top card first — never shown to the browser
	Player []cards.Card `json:"player"`
	Dealer []cards.Card `json:"dealer"`

	Bet     int64 `json:"bet"` // chips at risk; doubles on a double-down
	Doubled bool  `json:"doubled"`

	Phase   Phase   `json:"phase"`
	Outcome Outcome `json:"outcome"`

	// Payout is what returns to the player's chip stack when the hand is done:
	// stake plus winnings, net of rake. Zero on a loss. Rake is the house's cut,
	// recorded so the ledger can account for every chip that left the table.
	Payout int64 `json:"payout"`
	Rake   int64 `json:"rake"`
}

// Event is something the table can narrate or animate. The engine emits them
// instead of drawing anything itself.
type Event struct {
	Kind string      `json:"kind"` // "deal" | "player_card" | "dealer_card" | "reveal" | "settle"
	Card *cards.Card `json:"card,omitempty"`
	Text string      `json:"text,omitempty"`
}

// Move is a player action.
type Move string

const (
	Hit    Move = "hit"
	Stand  Move = "stand"
	Double Move = "double"
)

// HandValue totals a hand, counting each ace as 11 until that would bust, then
// demoting them one at a time. soft reports whether an ace is still counting as
// 11 — which is what makes "soft 17" a different thing from 17.
func HandValue(hand []cards.Card) (total int, soft bool) {
	aces := 0
	for _, c := range hand {
		switch {
		case c.Rank == cards.Ace:
			aces++
			total += 11
		case c.Rank >= 10:
			total += 10
		default:
			total += int(c.Rank)
		}
	}
	for total > 21 && aces > 0 {
		total -= 10 // demote an ace from 11 to 1
		aces--
	}
	return total, aces > 0
}

// IsBlackjack reports a natural: 21 on the opening two cards. A 21 assembled
// from three cards is not one, and does not get paid 3:2.
func IsBlackjack(hand []cards.Card) bool {
	if len(hand) != 2 {
		return false
	}
	v, _ := HandValue(hand)
	return v == 21
}

// New deals a fresh hand: two to the player, two to the dealer. If either side
// has a natural the hand is already over and the returned State is settled — a
// player with blackjack never gets asked whether they'd like to hit.
func New(bet int64, r Rules, rng *rand.Rand) (State, []Event, error) {
	if bet <= 0 {
		return State{}, nil, ErrBadBet
	}
	if r.Decks < 1 {
		r.Decks = 1
	}
	deck := cards.NewDeck(r.Decks)
	deck.Shuffle(rng)

	s := State{Rules: r, Deck: deck, Bet: bet, Phase: PhasePlayer}
	evs := []Event{{Kind: "deal"}}

	for i := 0; i < 2; i++ {
		if err := s.draw(&s.Player, "player_card", &evs); err != nil {
			return State{}, nil, err
		}
		if err := s.draw(&s.Dealer, "dealer_card", &evs); err != nil {
			return State{}, nil, err
		}
	}

	// A natural on either side ends it before the player ever acts.
	if IsBlackjack(s.Player) || IsBlackjack(s.Dealer) {
		s.settle(&evs)
	}
	return s, evs, nil
}

// draw takes one card off the shoe onto the given hand and records the event.
// Pointer receiver: it mutates the deck and the hand together, and neither may
// end up applied to a stale copy of the state.
func (s *State) draw(hand *[]cards.Card, kind string, evs *[]Event) error {
	c, ok := s.Deck.Draw()
	if !ok {
		return ErrDeckExhausted
	}
	*hand = append(*hand, c)
	card := c
	*evs = append(*evs, Event{Kind: kind, Card: &card})
	return nil
}

// ApplyMove is the whole engine: a legal move in, a new state and the events it
// produced out. An error means the move was illegal and the state is unchanged.
//
// s is taken by value, so the caller's state is only replaced on success.
func ApplyMove(s State, m Move) (State, []Event, error) {
	if s.Phase == PhaseDone {
		return s, nil, ErrHandOver
	}
	// A copied State still shares its slices' backing arrays with the original.
	// Two moves applied from the same starting state would then append cards over
	// each other. Clone first: the caller's state is genuinely untouched, and a
	// state can be replayed as many times as we like.
	s = s.clone()
	if s.Phase != PhasePlayer {
		return s, nil, ErrNotYourTurn
	}
	if m == Double && len(s.Player) != 2 {
		// Doubling means doubling the stake for exactly one more card. Only ever
		// legal on the opening two — after that you're just describing a hit.
		return s, nil, ErrCantDouble
	}
	if m != Hit && m != Stand && m != Double {
		return s, nil, ErrUnknownMove
	}

	evs := []Event{}

	if m == Double {
		s.Bet *= 2
		s.Doubled = true
	}

	if m == Hit || m == Double {
		if err := s.draw(&s.Player, "player_card", &evs); err != nil {
			return s, nil, err
		}
		if v, _ := HandValue(s.Player); v > 21 {
			s.settle(&evs) // bust; the dealer never has to play
			return s, evs, nil
		}
		if m == Hit {
			return s, evs, nil // still the player's turn
		}
	}

	// Stand, or a double that survived its card: the dealer draws out.
	s.Phase = PhaseDealer
	s.dealerPlay(&evs)
	return s, evs, nil
}

// dealerPlay draws the dealer out to the house rule, then settles. The dealer
// has no choices to make — that's the game — so this needs no move.
func (s *State) dealerPlay(evs *[]Event) {
	*evs = append(*evs, Event{Kind: "reveal"}) // the hole card turns over
	for {
		v, soft := HandValue(s.Dealer)
		hitSoft17 := s.Rules.DealerHitsSoft17 && v == 17 && soft
		if v >= 17 && !hitSoft17 {
			break
		}
		if err := s.draw(&s.Dealer, "dealer_card", evs); err != nil {
			break // shoe ran dry mid-draw; settle on what's on the table
		}
	}
	s.settle(evs)
}

// settle decides the outcome and the payout, and is the only place chips are
// computed.
//
// The rake comes off winnings, never off the stake: a player who pushes gets
// exactly their bet back, and a player who loses is never charged for the
// privilege. The house only takes a cut of money the house was going to hand
// over anyway. That's a rake, as opposed to a fee for showing up.
func (s *State) settle(evs *[]Event) {
	playerVal, _ := HandValue(s.Player)
	dealerVal, _ := HandValue(s.Dealer)
	playerBJ := IsBlackjack(s.Player)
	dealerBJ := IsBlackjack(s.Dealer)

	// profit is what the player wins on top of their stake. Negative means the
	// stake is gone.
	var profit int64

	switch {
	case playerVal > 21:
		s.Outcome = OutcomeBust
		profit = -s.Bet
	case playerBJ && dealerBJ:
		s.Outcome = OutcomePush
	case playerBJ:
		s.Outcome = OutcomeBlackjack
		profit = int64(math.Floor(float64(s.Bet) * s.Rules.BlackjackPays))
	case dealerBJ:
		s.Outcome = OutcomeLose
		profit = -s.Bet
	case dealerVal > 21:
		s.Outcome = OutcomeDealerBust
		profit = s.Bet
	case playerVal > dealerVal:
		s.Outcome = OutcomeWin
		profit = s.Bet
	case playerVal == dealerVal:
		s.Outcome = OutcomePush
	default:
		s.Outcome = OutcomeLose
		profit = -s.Bet
	}

	if profit > 0 {
		s.Rake = int64(math.Floor(float64(profit) * s.Rules.RakePct))
		if s.Rake < 0 {
			s.Rake = 0
		}
		profit -= s.Rake
	}
	if profit < 0 {
		s.Payout = 0 // stake is lost; nothing comes back
	} else {
		s.Payout = s.Bet + profit
	}

	s.Phase = PhaseDone
	*evs = append(*evs, Event{Kind: "settle", Text: string(s.Outcome)})
}

// Net is what the hand did to the player's chip stack: payout minus the stake
// they put up. Negative on a loss, zero on a push.
func (s State) Net() int64 {
	if s.Phase != PhaseDone {
		return 0
	}
	return s.Payout - s.Bet
}

// CanDouble reports whether Double is legal right now — the shell asks this to
// decide whether to light the button up.
func (s State) CanDouble() bool {
	return s.Phase == PhasePlayer && len(s.Player) == 2
}

// clone deep-copies the slices so a derived state shares no backing array with
// the one it came from.
func (s State) clone() State {
	s.Deck = append(cards.Deck(nil), s.Deck...)
	s.Player = append([]cards.Card(nil), s.Player...)
	s.Dealer = append([]cards.Card(nil), s.Dealer...)
	return s
}
