package web

import (
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"

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

// Texas hold'em, played for chips against the trained bots.
//
// This is the only table in the casino that is a *session* rather than a game.
// Everywhere else you stake, you play once, and a multiple pays: the hand is the
// unit and the money moves at both ends of it. Poker is not that shape. You buy
// chips onto the table, you play as many hands as you feel like, and you leave
// with whatever is in front of you — so the live row lives across hands, and the
// chips move exactly twice: once when you sit down, once when you get up.
//
// Which means there is no "payout" until you stand up, and `commit` is only ever
// told the game is Done when you do (or when you have nothing left to sit with).
// In between, every pot won and lost is inside the engine, and storage sees none
// of it. That is the honest model, and it is also the safe one: a hand that dies
// halfway leaves the chips where they were, on the table, in the live row.
//
// What the browser is allowed to see: your two cards, the board, everybody's
// stacks and bets, and nothing else. Not the deck, and not a bot's hand — until
// a showdown turns it over, which is the moment it stops being a secret.

// holdemSeatView is one seat. Cards are present only when the viewer is entitled
// to them: yours always, a bot's never, until the hand is shown down.
type holdemSeatView struct {
	Name  string     `json:"name"`
	Bot   bool       `json:"bot"`
	You   bool       `json:"you"`
	Stack int64      `json:"stack"`
	Bet   int64      `json:"bet"`
	State string     `json:"state"` // active | folded | allin | out
	Pos   string     `json:"pos"`   // BTN, SB, BB, UTG…
	Cards []cardView `json:"cards,omitempty"`
	Won   int64      `json:"won,omitempty"`
}

var seatStates = map[holdem.SeatState]string{
	holdem.Active: "active",
	holdem.Folded: "folded",
	holdem.AllIn:  "allin",
	holdem.Out:    "out",
}

// holdemView is the table as its player may see it.
type holdemView struct {
	Tier   holdem.Tier      `json:"tier"`
	Seats  []holdemSeatView `json:"seats"`
	Button int              `json:"button"`
	HandNo int              `json:"hand_no"`

	Board  []cardView `json:"board"`
	Street string     `json:"street"`
	Pot    int64      `json:"pot"`
	Side   []int64    `json:"side,omitempty"`

	ToAct int    `json:"to_act"`
	Phase string `json:"phase"`

	// What you may do, decided here rather than in the browser — the felt should
	// never offer a button the table would refuse.
	Owed     int64 `json:"owed"`
	CanCheck bool  `json:"can_check"`
	CanRaise bool  `json:"can_raise"`
	MinRaise int64 `json:"min_raise_to"`
	MaxRaise int64 `json:"max_raise_to"`

	Stack    int64 `json:"stack"` // what's in front of you
	BoughtIn int64 `json:"bought_in"`
	Rake     int64 `json:"rake"` // what the house has taken this session
	MaxTopUp int64 `json:"max_topup"`
	Payout   int64 `json:"payout,omitempty"`
}

func viewHoldem(g holdem.State) holdemView {
	v := holdemView{
		Tier:     g.Tier,
		Button:   g.Button,
		HandNo:   g.HandNo,
		Street:   g.Street.String(),
		Pot:      g.Total(),
		ToAct:    g.ToAct,
		Phase:    string(g.Phase),
		Stack:    g.Seats[holdem.You].Stack,
		BoughtIn: g.BoughtIn,
		Rake:     g.Paid, // the part you actually paid, not the part the table lifted
		Payout:   g.Payout,
	}
	for _, p := range g.Side {
		v.Side = append(v.Side, p.Amount)
	}
	// An empty board is an empty board, not null. A Go slice with nothing in it
	// marshals to null, and a browser that has to write `(board || [])` everywhere
	// is a browser one forgotten guard away from a crash on every preflop.
	v.Board = []cardView{}
	for _, c := range g.Community {
		v.Board = append(v.Board, viewCard(c))
	}

	// The wall. A bot's hand crosses the wire in exactly one situation — the hand
	// was shown down and they did not fold — because that is the only situation in
	// which a player at a real table would be looking at it.
	shown := g.Street == holdem.Showdown
	for i, p := range g.Seats {
		seat := holdemSeatView{
			Name:  p.Name,
			Bot:   p.Bot,
			You:   i == holdem.You,
			Stack: p.Stack,
			Bet:   p.Bet,
			State: seatStates[p.State],
			Pos:   g.Position(i),
			Won:   p.Won,
		}
		mine := i == holdem.You
		dealt := p.State != holdem.Out && p.Hole[0].Rank != 0
		if dealt && (mine || (shown && p.State != holdem.Folded)) {
			seat.Cards = []cardView{viewCard(p.Hole[0]), viewCard(p.Hole[1])}
		}
		v.Seats = append(v.Seats, seat)
	}

	if g.Phase == holdem.PhaseBetting && g.ToAct == holdem.You {
		v.Owed = g.Owed(holdem.You)
		v.CanCheck = v.Owed == 0
		v.CanRaise = g.CanRaise(holdem.You)
		v.MinRaise = g.MinRaiseTo(holdem.You)
		v.MaxRaise = g.MaxRaiseTo(holdem.You)
	}
	if top := g.Tier.MaxBuy - g.Seats[holdem.You].Stack; top > 0 {
		v.MaxTopUp = top
	}
	return v
}

// holdemEventView is one beat of the script the felt plays back. The engine only
// ever attaches a bot's cards to a showdown; this drops them again anywhere else,
// which is the second of the two walls.
type holdemEventView struct {
	Kind   string     `json:"kind"`
	Seat   int        `json:"seat"`
	Cards  []cardView `json:"cards,omitempty"`
	Amount int64      `json:"amount,omitempty"`
	Total  int64      `json:"total,omitempty"`
	Text   string     `json:"text,omitempty"`
}

func viewHoldemEvents(evs []holdem.Event) []holdemEventView {
	out := make([]holdemEventView, 0, len(evs))
	for _, e := range evs {
		v := holdemEventView{Kind: e.Kind, Seat: e.Seat, Amount: e.Amount, Total: e.Total, Text: e.Text}
		for _, c := range e.Cards {
			v.Cards = append(v.Cards, viewCard(c))
		}
		// A card may ride an event only if it is the board, your own hand, or a hand
		// being shown down. Anything else is a bot's business.
		if len(v.Cards) > 0 && e.Seat >= 0 && e.Seat != holdem.You && e.Kind != "show" {
			v.Cards = nil
		}
		out = append(out, v)
	}
	return out
}

// handleHoldemSit buys chips onto a table. The chips are staked first, in the
// same statement that checks they exist, so two sit-downs fired at once cannot
// buy in with the same chip.
func (s *Server) handleHoldemSit(w http.ResponseWriter, r *http.Request) {
	user, ok := s.player(w, r)
	if !ok {
		return
	}
	var req struct {
		Tier  string `json:"tier"`
		Bots  int    `json:"bots"`
		BuyIn int64  `json:"buyin"`
	}
	if err := decodeJSON(r, &req); err != nil {
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}
	tier, err := holdem.TierBySlug(req.Tier)
	if err != nil {
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "pick a table"})
		return
	}
	if req.BuyIn < tier.MinBuy || req.BuyIn > tier.MaxBuy {
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{
			"error": "that isn't a legal buy-in for this table",
		})
		return
	}
	if req.Bots < 1 || req.Bots > holdem.MaxBots {
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "pick some opponents"})
		return
	}

	if err := storage.Stake(user, req.BuyIn); err != nil {
		if errors.Is(err, storage.ErrInsufficientChips) || errors.Is(err, storage.ErrBadAmount) {
			writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "not enough chips to sit down"})
			return
		}
		slog.Error("games: holdem buy-in", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	seed1, seed2 := newSeeds()
	g, evs, err := holdem.New(tier, req.Bots, req.BuyIn, blackjack.DefaultRules().RakePct, seed1, seed2)
	if err != nil {
		_ = storage.Award(user, req.BuyIn) // nobody sat down, so nothing was bought
		slog.Error("games: holdem sit", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	s.persistHoldem(w, user, g, evs, seed1, seed2, true)
}

// handleHoldemMove plays one move: an action in a hand, or the three that are
// about the session — deal the next hand, put more chips on the table, get up.
func (s *Server) handleHoldemMove(w http.ResponseWriter, r *http.Request) {
	user, ok := s.player(w, r)
	if !ok {
		return
	}
	var move holdem.Move
	if err := decodeJSON(r, &move); err != nil {
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}

	live, err := storage.LoadLiveHand(user)
	if errors.Is(err, storage.ErrNoLiveHand) {
		writeJSONStatus(w, http.StatusConflict, map[string]string{"error": "you're not at a table"})
		return
	}
	if err != nil {
		slog.Error("games: holdem load", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	if live.Game != gameHoldem {
		writeJSONStatus(w, http.StatusConflict, map[string]string{"error": "finish the game you're in first"})
		return
	}
	var g holdem.State
	if err := json.Unmarshal(live.State, &g); err != nil {
		slog.Error("games: unreadable holdem table", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	// A top-up is real money crossing the border, so the chips come off the stack
	// before the engine is asked — and go straight back if it says no. Same order,
	// and the same reason, as doubling down at blackjack.
	topped := int64(0)
	if move.Kind == holdem.TopUp {
		if err := storage.Stake(user, move.Amount); err != nil {
			if errors.Is(err, storage.ErrInsufficientChips) || errors.Is(err, storage.ErrBadAmount) {
				writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "you don't have those chips"})
				return
			}
			slog.Error("games: holdem top-up", "user", user, "err", err)
			http.Error(w, "internal error", http.StatusInternalServerError)
			return
		}
		topped = move.Amount
	}

	next, evs, err := holdem.ApplyMove(g, move)
	if err != nil {
		if topped > 0 {
			_ = storage.Award(user, topped) // the top-up didn't happen
		}
		msg := "that move isn't legal here"
		switch {
		case errors.Is(err, holdem.ErrHandLive):
			msg = "finish the hand first"
		case errors.Is(err, holdem.ErrNotYourTurn):
			msg = "it isn't your turn"
		case errors.Is(err, holdem.ErrCantCheck):
			msg = "there's a bet to you"
		case errors.Is(err, holdem.ErrTooSmall):
			msg = "that's under the minimum raise"
		case errors.Is(err, holdem.ErrTooBig):
			msg = "you don't have that many chips"
		case errors.Is(err, holdem.ErrBadBuyIn):
			msg = "that would put you over the table maximum"
		}
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": msg})
		return
	}
	s.persistHoldem(w, user, next, evs, live.Seed1, live.Seed2, false)
}

// persistHoldem writes the table back and answers the browser.
//
// The session settles exactly once — when the player gets up, or when they have
// nothing left to get up with. Until then Done is false and `commit` moves no
// chips at all, which is what makes a hundred hands of poker a single trip across
// the border rather than a hundred.
func (s *Server) persistHoldem(w http.ResponseWriter, user string, g holdem.State, evs []holdem.Event, seed1, seed2 uint64, fresh bool) {
	blob, err := json.Marshal(g)
	if err != nil {
		slog.Error("games: marshal holdem", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	done := g.Phase == holdem.PhaseDone
	outcome := "left"
	switch {
	case done && g.Payout == 0:
		outcome = "busted"
	case done && g.Payout > g.BoughtIn:
		outcome = "up"
	case done && g.Payout < g.BoughtIn:
		outcome = "down"
	}

	v, ok := s.commit(w, user, finished{
		Game: gameHoldem, Blob: blob,
		// Paid, not Rake: the audit log is the house's income, and the house only
		// makes money off the player. What it lifts off a bot's pot is not income.
		Bet: g.BoughtIn, Payout: g.Payout, Rake: g.Paid,
		Outcome: outcome, Done: done,
		Seed1: seed1, Seed2: seed2, Fresh: fresh,
	})
	if !ok {
		return
	}
	// A closed session is gone from storage, so the table view has none to show —
	// but the browser still needs the last board to land the verdict on.
	if done {
		hv := viewHoldem(g)
		v.Holdem = &hv
	}
	v.HoldemEvents = viewHoldemEvents(evs)
	writeJSON(w, v)
}
