package web

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

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

// UNO, played for chips against bots.
//
// The seam is the same as every other table, but there is one thing here that no
// other table has: opponents. The obvious way to give a browser opponents is a
// socket, and the plan says solo UNO must not need one — so it doesn't. A move
// goes up, and what comes back is the player's move *plus every bot turn it
// handed off to*, as a script of events. One request, one round of the table.
//
// What the browser is allowed to see: its own hand, the card in play, the colour
// in play, and how many cards each bot is holding. Not the deck, not a bot's
// hand, not even the face of a card a bot drew. That last one is most of the
// deck, and it is the thing that would turn a game of counting cards into a game
// of reading the network tab.

// unoCardView is one card, ready to draw. The browser gets the colour and the
// face as words, not as the engine's integers — the same bargain the blackjack
// table makes, and for the same reason: the browser draws faces, not logic.
type unoCardView struct {
	Color string `json:"color"` // "red" | "blue" | "yellow" | "green" | "wild"
	Value string `json:"value"` // "0"…"9" | "skip" | "reverse" | "+2" | "wild" | "+4"
	Wild  bool   `json:"wild"`  // it's a wild, whatever colour it was played as
}

func viewUnoCard(c uno.Card) unoCardView {
	return unoCardView{
		Color: c.Color.String(),
		Value: c.Value.String(),
		Wild:  c.IsWild(),
	}
}

// unoSeatView is one seat at the table: a name, and a number of cards. A bot's
// cards are a *count*. There is no field here for what they are.
type unoSeatView struct {
	Name   string `json:"name"`
	Cards  int    `json:"cards"`
	You    bool   `json:"you"`
	Uno    bool   `json:"uno"`    // down to one card
	Called bool   `json:"called"` // …and said so. A seat where Uno is true and this isn't is a seat you can catch
	Out    bool   `json:"out"`    // No Mercy: buried at twenty-five, and not coming back
}

// unoView is a game as its player may see it.
type unoView struct {
	Tier  uno.Tier      `json:"tier"`
	Seats []unoSeatView `json:"seats"`

	Hand     []unoCardView `json:"hand"`     // yours, and only yours
	Playable []int         `json:"playable"` // which of them can legally go down
	Top      unoCardView   `json:"top"`      // the card in play
	Color    string        `json:"color"`    // the colour in play, which a wild renames
	Deck     int           `json:"deck"`     // cards left to draw

	// The UNO call. UnoAt is which of your cards would leave you holding exactly
	// one if you played it — the table asks for the call on those, and it asks the
	// engine which they are because No Mercy's "discard all" makes counting the
	// hand the wrong answer. Catchable is which seats are sitting on one card they
	// never announced.
	UnoAt     []int `json:"uno_at"`
	Catchable []int `json:"catchable"`

	Turn int `json:"turn"`
	Dir  int `json:"dir"`

	// No Mercy: the bill a stack of draw cards has run up. Whoever stops stacking
	// pays it, and while it stands it is the only thing on the table that matters —
	// so the felt has to be able to say what it is.
	Pending int `json:"pending"`

	Bet     int64  `json:"bet"`
	Pays    int64  `json:"pays"` // what going out right now would actually pay
	Phase   string `json:"phase"`
	Outcome string `json:"outcome,omitempty"`
	Winner  int    `json:"winner"`
	Payout  int64  `json:"payout,omitempty"`
	Rake    int64  `json:"rake,omitempty"`
	Net     int64  `json:"net"`
}

func viewUno(g uno.State) unoView {
	v := unoView{
		Tier:    g.Tier,
		Top:     viewUnoCard(g.Top()),
		Color:   g.Color.String(),
		Deck:    g.Left(),
		Turn:    g.Turn,
		Dir:     g.Dir,
		Pending: g.Pending,
		Bet:     g.Bet,
		Pays:    g.Pays(),
		Phase:   string(g.Phase),
		Outcome: string(g.Outcome),
		Winner:  -1,
		Payout:  g.Payout,
		Rake:    g.Rake,
		Net:     g.Net(),
	}
	// An empty hand is a seat that went out — *unless* the mercy rule took it, in
	// which case an empty hand is a grave. Those two look identical from the count
	// alone, which is why a buried seat is asked about rather than inferred.
	for i, n := range g.Counts() {
		live := g.Live(i)
		seat := unoSeatView{
			Cards: n, You: i == uno.You, Uno: live && n == 1, Out: !live,
			Called: i < len(g.Called) && g.Called[i],
		}
		if i == uno.You {
			seat.Name = "You"
		} else if i-1 < len(g.Bots) {
			seat.Name = g.Bots[i-1]
		}
		v.Seats = append(v.Seats, seat)
		if live && n == 0 {
			v.Winner = i
		}
	}
	// And you can win a No Mercy table without ever going out: outlive it, and the
	// last seat standing is you, with a hand still in it.
	if v.Winner < 0 && g.Outcome.Won() {
		v.Winner = uno.You
	}
	for _, c := range g.Hands[uno.You] {
		v.Hand = append(v.Hand, viewUnoCard(c))
	}
	v.Playable = g.Playable()
	if v.Playable == nil {
		v.Playable = []int{}
	}
	// Empty arrays, never null: the table indexes into these, and `null.indexOf`
	// is a broken game rather than a quiet no-op.
	v.UnoAt = g.UnoAt()
	if v.UnoAt == nil {
		v.UnoAt = []int{}
	}
	v.Catchable = g.Catchable()
	if v.Catchable == nil {
		v.Catchable = []int{}
	}
	return v
}

// unoEventView is one beat of the script the table plays back: a card going
// down, a seat eating a +4, the turn coming round. The engine's own events carry
// engine types, so they are re-rendered here rather than shipped raw — and this
// is also the wall where a bot's drawn card is dropped on the floor.
type unoEventView struct {
	Kind  string       `json:"kind"`
	Seat  int          `json:"seat"`
	Card  *unoCardView `json:"card,omitempty"`
	Color string       `json:"color,omitempty"`
	N     int          `json:"n,omitempty"`
	Left  int          `json:"left"` // never omitempty: a seat that goes out leaves zero, and zero is the number the table has to draw
	By    int          `json:"by"`   // who did the catching. Seat zero is you, and "you" is a real answer, so never omitempty either
	Text  string       `json:"text,omitempty"`

	// Hand is your hand as it stands after this event, on the events that changed
	// it. The table redraws your fan from this as the lap plays back — see the
	// engine's Event.Hand for why. It is your own hand, so there is nothing here
	// the browser wasn't already holding.
	Hand []unoCardView `json:"hand,omitempty"`
}

func viewUnoEvents(evs []uno.Event) []unoEventView {
	out := make([]unoEventView, 0, len(evs))
	for _, e := range evs {
		v := unoEventView{Kind: e.Kind, Seat: e.Seat, N: e.N, Left: e.Left, By: e.By, Text: e.Text}
		if e.Color != uno.Wild {
			v.Color = e.Color.String()
		}
		// The engine only stamps a hand on an event about seat zero. This is the belt
		// to that brace: whatever the engine thinks it's doing, no hand but yours
		// leaves this building.
		if e.Seat == uno.You && e.Hand != nil {
			v.Hand = make([]unoCardView, 0, len(e.Hand))
			for _, c := range e.Hand {
				v.Hand = append(v.Hand, viewUnoCard(c))
			}
		}
		if e.Card != nil {
			// The engine only ever attaches a card to an event the seat is entitled
			// to see it in — a card played face up, or one *you* drew. This check is
			// the belt to that pair of braces: a bot's draw never carries a face,
			// whatever the engine thinks it's doing.
			if e.Kind == uno.EvDraw || e.Kind == uno.EvForced {
				if e.Seat == uno.You {
					c := viewUnoCard(*e.Card)
					v.Card = &c
				}
			} else {
				c := viewUnoCard(*e.Card)
				v.Card = &c
			}
		}
		out = append(out, v)
	}
	return out
}

// handleUnoStart takes the bet and deals. Same order as every other table: the
// chips are staked first, in the same statement that checks they exist, so two
// deals fired at once cannot bet the same chip.
func (s *Server) handleUnoStart(w http.ResponseWriter, r *http.Request) {
	user, ok := s.player(w, r)
	if !ok {
		return
	}
	var req struct {
		Bet  int64  `json:"bet"`
		Tier string `json:"tier"`
	}
	if err := decodeJSON(r, &req); err != nil || req.Bet <= 0 {
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "bet something"})
		return
	}
	tier, err := uno.TierBySlug(req.Tier)
	if err != nil {
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "pick a table"})
		return
	}

	if err := storage.Stake(user, req.Bet); err != nil {
		if errors.Is(err, storage.ErrInsufficientChips) || errors.Is(err, storage.ErrBadAmount) {
			writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "not enough chips for that bet"})
			return
		}
		slog.Error("games: uno stake", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	seed1, seed2 := newSeeds()
	g, evs, err := uno.New(req.Bet, tier, blackjack.DefaultRules().RakePct, seed1, seed2)
	if err != nil {
		// The game never happened, so the stake never should have left.
		_ = storage.Award(user, req.Bet)
		slog.Error("games: uno deal", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	s.persistUno(w, user, g, evs, seed1, seed2, true)
}

// handleUnoMove plays one turn: a card, a draw, or passing on the card you drew.
// The bots' turns come back with it.
func (s *Server) handleUnoMove(w http.ResponseWriter, r *http.Request) {
	user, ok := s.player(w, r)
	if !ok {
		return
	}
	var move uno.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": "no game in progress"})
		return
	}
	if err != nil {
		slog.Error("games: uno load", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	if live.Game != gameUno {
		writeJSONStatus(w, http.StatusConflict, map[string]string{"error": "finish the game you're in first"})
		return
	}
	var g uno.State
	if err := json.Unmarshal(live.State, &g); err != nil {
		slog.Error("games: unreadable uno game", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	next, evs, err := uno.ApplyMove(g, move)
	if err != nil {
		// The refusals a player can actually cause, said in words rather than as
		// "that move isn't legal here" — which, in a game with this many rules, is
		// the table refusing to explain itself.
		msg := "that move isn't legal here"
		switch {
		case errors.Is(err, uno.ErrCantPlay):
			msg = "that card doesn't go on this one"
		case errors.Is(err, uno.ErrNeedColor):
			msg = "pick a colour for the wild"
		case errors.Is(err, uno.ErrMustPlayNow):
			msg = "play the card you drew, or pass"
		case errors.Is(err, uno.ErrCantPass):
			msg = "draw first, then you can pass"
		case errors.Is(err, uno.ErrMustStack):
			msg = "answer the stack with a draw card, or take it"
		case errors.Is(err, uno.ErrNoStack):
			msg = "there's nothing pointed at you to take"
		case errors.Is(err, uno.ErrNoCatch):
			msg = "there's nobody in that seat to catch"
		}
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": msg})
		return
	}
	s.persistUno(w, user, next, evs, live.Seed1, live.Seed2, false)
}

// persistUno writes the game back and answers the browser.
func (s *Server) persistUno(w http.ResponseWriter, user string, g uno.State, evs []uno.Event, seed1, seed2 uint64, fresh bool) {
	blob, err := json.Marshal(g)
	if err != nil {
		slog.Error("games: marshal uno", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	done := g.Phase == uno.PhaseDone
	v, ok := s.commit(w, user, finished{
		Game: gameUno, Blob: blob,
		Bet: g.Bet, Payout: g.Payout, Rake: g.Rake,
		Outcome: string(g.Outcome), Done: done,
		Seed1: seed1, Seed2: seed2, Fresh: fresh,
	})
	if !ok {
		return
	}
	// A finished game is gone from storage, so the table has none to show — but the
	// browser still needs the final board to land the verdict on.
	if done {
		uv := viewUno(g)
		v.Uno = &uv
	}
	v.UnoEvents = viewUnoEvents(evs)
	writeJSON(w, v)
}
