// 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 (
	"encoding/json"
	"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")
	ErrCantSplit     = errors.New("blackjack: split is only allowed on two cards of the same rank")
	ErrDeckExhausted = errors.New("blackjack: the shoe is empty")
	ErrBadBet        = errors.New("blackjack: bet must be positive")
)

// MaxHands is how many hands one deal can turn into: the opening hand, plus
// three splits. Four is the usual house limit and it is also the point past
// which the felt runs out of room.
const MaxHands = 4

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

const (
	PhasePlayer Phase = "player" // the player is acting on the active hand
	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}
}

// Hand is one hand the player is holding, with the chips that are on it.
//
// A deal starts with one. A split turns one into two, and the new hand carries a
// bet of its own — which is the whole reason split is not just a card trick: it
// is the only move in the game that takes more chips out of a player's stack
// *after* the cards are out.
type Hand struct {
	Cards   []cards.Card `json:"cards"`
	Bet     int64        `json:"bet"`
	Doubled bool         `json:"doubled"`

	// Split marks a hand that came out of a split. It exists for one rule: 21 on
	// a split hand is twenty-one, not a natural, and is paid 1:1 like any other.
	// Otherwise splitting aces would print money.
	Split bool `json:"split"`

	// Done means this hand will not be acted on again: it stood, it busted, it
	// doubled, or it is a split ace, which gets exactly one card and no say.
	Done bool `json:"done"`

	Outcome Outcome `json:"outcome"`
	Payout  int64   `json:"payout"` // stake plus winnings, net of rake. Zero on a loss.
	Rake    int64   `json:"rake"`
}

// Value totals the hand. See HandValue.
func (h Hand) Value() (int, bool) { return HandValue(h.Cards) }

// Natural reports a blackjack: 21 on the opening two cards of a hand that was
// dealt, not split.
func (h Hand) Natural() bool { return !h.Split && IsBlackjack(h.Cards) }

// State is one deal of heads-up blackjack: the player's hands against the
// dealer's one.
type State struct {
	Rules  Rules        `json:"rules"`
	Deck   cards.Deck   `json:"deck"` // the shoe, top card first — never shown to the browser
	Dealer []cards.Card `json:"dealer"`

	// Hands is always at least one, and Active indexes the one being played. The
	// player works left to right: a hand is finished before the next is looked at,
	// which is both how a real table does it and what keeps the felt legible.
	Hands  []Hand `json:"hands"`
	Active int    `json:"active"`

	Phase   Phase   `json:"phase"`
	Outcome Outcome `json:"outcome"` // the deal as a whole; per-hand outcomes live on the hands

	// Bet, Payout and Rake are the totals across every hand: what the player has
	// staked, what comes back, and what the house kept. The ledger and the chip
	// stack only ever deal in these.
	Bet    int64 `json:"bet"`
	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.
//
// Hand is which of the player's hands an event landed on — meaningless for the
// dealer's, and the reason it is here at all is that after a split the browser
// has to know which fan a card is flying to.
type Event struct {
	Kind string      `json:"kind"` // "deal" | "player_card" | "dealer_card" | "split" | "double" | "reveal" | "settle"
	Card *cards.Card `json:"card,omitempty"`
	Hand int         `json:"hand"`
	Text string      `json:"text,omitempty"`
}

// Move is a player action.
type Move string

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

// 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,
		Hands: []Hand{{Bet: bet}},
		Bet:   bet,
		Phase: PhasePlayer,
	}
	evs := []Event{{Kind: "deal"}}

	for i := 0; i < 2; i++ {
		if err := s.hit(0, &evs); err != nil {
			return State{}, nil, err
		}
		if err := s.drawDealer(&evs); err != nil {
			return State{}, nil, err
		}
	}

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

// hit puts one card on a player hand. 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) hit(i int, evs *[]Event) error {
	c, ok := s.Deck.Draw()
	if !ok {
		return ErrDeckExhausted
	}
	s.Hands[i].Cards = append(s.Hands[i].Cards, c)
	card := c
	*evs = append(*evs, Event{Kind: "player_card", Card: &card, Hand: i})
	return nil
}

func (s *State) drawDealer(evs *[]Event) error {
	c, ok := s.Deck.Draw()
	if !ok {
		return ErrDeckExhausted
	}
	s.Dealer = append(s.Dealer, c)
	card := c
	*evs = append(*evs, Event{Kind: "dealer_card", 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
	}
	switch m {
	case Hit, Stand, Double, Split:
	default:
		return s, nil, ErrUnknownMove
	}
	if m == Double && !s.CanDouble() {
		// 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 == Split && !s.CanSplit() {
		return s, nil, ErrCantSplit
	}

	i := s.Active
	evs := []Event{}

	switch m {
	case Split:
		// The second card moves to a hand of its own, carrying a bet the same size
		// as the one it came from, and both hands are topped up to two cards.
		h := &s.Hands[i]
		moved := h.Cards[1]
		h.Cards = h.Cards[:1]
		h.Split = true

		fresh := Hand{Cards: []cards.Card{moved}, Bet: h.Bet, Split: true}
		s.Hands = append(s.Hands, Hand{})
		copy(s.Hands[i+2:], s.Hands[i+1:]) // the new hand sits immediately to the right
		s.Hands[i+1] = fresh
		s.Bet += fresh.Bet

		evs = append(evs, Event{Kind: "split", Hand: i})
		if err := s.hit(i, &evs); err != nil {
			return s, nil, err
		}
		if err := s.hit(i+1, &evs); err != nil {
			return s, nil, err
		}

		// Split aces get one card each and no say in it. Without this rule a pair
		// of aces is the best hand in the game and everybody would split them
		// forever; with it, splitting aces is a gamble like everything else.
		if moved.Rank == cards.Ace {
			s.Hands[i].Done = true
			s.Hands[i+1].Done = true
		}
		// A hand that has just been dealt a card can still be sitting on 21, and a
		// 21 has nothing left to decide.
		s.finishIfDone(i)
		s.finishIfDone(i + 1)

	case Double:
		h := &s.Hands[i]
		s.Bet += h.Bet
		h.Bet *= 2
		h.Doubled = true
		// Announced before the card, because that is the order it happens in: the
		// chips go down, and *then* you find out what you bought with them.
		evs = append(evs, Event{Kind: "double", Hand: i})
		if err := s.hit(i, &evs); err != nil {
			return s, nil, err
		}
		h.Done = true // one card, and that is the deal you made

	case Hit:
		if err := s.hit(i, &evs); err != nil {
			return s, nil, err
		}
		s.finishIfDone(i)

	case Stand:
		s.Hands[i].Done = true
	}

	s.advance(&evs)
	return s, evs, nil
}

// finishIfDone closes a hand that has nothing left to decide: it busted, or it
// is sitting on 21 and would only be hitting it to be polite.
func (s *State) finishIfDone(i int) {
	if v, _ := s.Hands[i].Value(); v >= 21 {
		s.Hands[i].Done = true
	}
}

// advance moves to the next hand still owed a decision. When there are none, the
// dealer plays — unless every hand busted, in which case there is nothing to beat
// and the dealer does not draw.
func (s *State) advance(evs *[]Event) {
	for i := s.Active; i < len(s.Hands); i++ {
		if !s.Hands[i].Done {
			s.Active = i
			return
		}
	}
	s.Active = len(s.Hands) - 1

	if s.allBust() {
		// The dealer draws no cards, but the hole card still turns over: the
		// browser has been showing a face-down card since the deal, and the settled
		// state it is about to be handed has the dealer's full total on it. Without
		// the reveal you get a nineteen printed under a card nobody has looked at.
		*evs = append(*evs, Event{Kind: "reveal"})
		s.settle(evs)
		return
	}
	s.Phase = PhaseDealer
	s.dealerPlay(evs)
}

func (s *State) allBust() bool {
	for _, h := range s.Hands {
		if v, _ := h.Value(); v <= 21 {
			return false
		}
	}
	return true
}

// 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.drawDealer(evs); err != nil {
			break // shoe ran dry mid-draw; settle on what's on the table
		}
	}
	s.settle(evs)
}

// settle decides every hand against the dealer and adds up what comes back. It
// 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.
//
// Each hand is raked on its own winnings. Netting the hands against each other
// first would let a player who won one and lost one pay no rake at all, which is
// not a rake, it's a discount for splitting.
func (s *State) settle(evs *[]Event) {
	dealerVal, _ := HandValue(s.Dealer)
	dealerBJ := IsBlackjack(s.Dealer)

	s.Payout, s.Rake = 0, 0
	for i := range s.Hands {
		h := &s.Hands[i]
		playerVal, _ := h.Value()

		// profit is what this hand wins on top of its stake. Negative means the
		// stake is gone.
		var profit int64

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

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

		s.Payout += h.Payout
		s.Rake += h.Rake
	}

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

// overall is the deal's outcome as one word, which is what the ledger and the
// history line want. With one hand it is simply that hand's. With several there
// is no honest single word for "won one, lost one", so it reports what the deal
// did to the player's chips, which is the thing anybody actually means.
func (s State) overall() Outcome {
	if len(s.Hands) == 1 {
		return s.Hands[0].Outcome
	}
	switch net := s.Payout - s.Bet; {
	case net > 0:
		return OutcomeWin
	case net < 0:
		return OutcomeLose
	default:
		return OutcomePush
	}
}

// Net is what the deal did to the player's chip stack: everything paid out minus
// everything staked. Negative on a loss, zero on a push.
func (s State) Net() int64 {
	if s.Phase != PhaseDone {
		return 0
	}
	return s.Payout - s.Bet
}

// Hand returns the hand being played. There is always one.
func (s State) Hand() Hand {
	if s.Active < 0 || s.Active >= len(s.Hands) {
		return Hand{}
	}
	return s.Hands[s.Active]
}

// 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 {
	h := s.Hand()
	return s.Phase == PhasePlayer && !h.Done && len(h.Cards) == 2
}

// CanSplit reports whether Split is legal: two cards of the same rank, and room
// at the table for another hand.
//
// Same *rank*, not same value: a king and a queen are both worth ten and are not
// a pair, which is the stricter of the two house rules and the one that doesn't
// need explaining on the felt.
func (s State) CanSplit() bool {
	h := s.Hand()
	if s.Phase != PhasePlayer || h.Done || len(h.Cards) != 2 || len(s.Hands) >= MaxHands {
		return false
	}
	return h.Cards[0].Rank == h.Cards[1].Rank
}

// SplitCost is what splitting the active hand would take out of the player's
// stack: another bet the same size as the one already on it. The shell has to
// take these chips before the move, because a split the player cannot cover is
// not a legal move.
func (s State) SplitCost() int64 { return s.Hand().Bet }

// DoubleCost is the same idea for a double: the stake again.
func (s State) DoubleCost() int64 { return s.Hand().Bet }

// 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.Dealer = append([]cards.Card(nil), s.Dealer...)
	hands := make([]Hand, len(s.Hands))
	copy(hands, s.Hands)
	for i := range hands {
		hands[i].Cards = append([]cards.Card(nil), hands[i].Cards...)
	}
	s.Hands = hands
	return s
}

// UnmarshalJSON reads a state, including one written before split existed.
//
// A live hand outlives a deploy: it is a blob in the database, and somebody is
// mid-hand when the new binary starts. Those blobs have a "player" array and no
// "hands", and without this they would come back as a state with no hands at all
// — which is not a decoding error, it's a player whose cards vanished. So the old
// shape is read as what it always was: one hand, holding the whole stake.
func (s *State) UnmarshalJSON(b []byte) error {
	type state State // shed the method, or this recurses forever
	var v struct {
		state
		Player []cards.Card `json:"player"`
	}
	if err := json.Unmarshal(b, &v); err != nil {
		return err
	}
	*s = State(v.state)
	if len(s.Hands) == 0 && len(v.Player) > 0 {
		s.Hands = []Hand{{
			Cards:   v.Player,
			Bet:     s.Bet,
			Outcome: s.Outcome,
			Payout:  s.Payout,
			Rake:    s.Rake,
			Done:    s.Phase == PhaseDone,
		}}
		s.Active = 0
	}
	return nil
}
