package web

import (
	"encoding/json"
	"errors"
	"log/slog"
	"math/rand/v2"
	"net/http"

	"pete/internal/games/blackjack"
	"pete/internal/games/cards"
	"pete/internal/games/klondike"
	"pete/internal/storage"
)

// Solitaire, played for chips. Vegas scoring: you buy the deck, and every card
// you get home pays a slice of it back.
//
// The withheld information here is bigger than blackjack's single hole card —
// it's the stock and every face-down card in the tableau, which between them are
// most of the deck. So the view sends *counts* for both: a column says how many
// cards are face-down under it, never which. A browser that held the stock would
// be a browser that knows whether the next pull is worth taking, and this game is
// nothing but that decision, repeated.
//
// The events, on the other hand, need no filtering at all, and that's worth
// saying out loud because blackjack's do. Every card a klondike event carries is
// a card the move itself just turned face up: the draw puts cards in the waste,
// the flip turns a column's top card over, a move carries cards that were already
// face up. There is no event here that mentions a card the player isn't now
// looking at.

// solPileView is one tableau column: how deep the face-down stack is, and the
// run sitting face up on top of it.
type solPileView struct {
	Down int        `json:"down"`
	Up   []cardView `json:"up"`
}

// solFoundView is one foundation. Only the top card matters — it's the only one
// that can be played back off — so it's the only one sent, with a count for the
// height of the pile.
type solFoundView struct {
	Suit string    `json:"suit"` // the glyph, so the empty pile can show what it wants
	Red  bool      `json:"red"`
	N    int       `json:"n"`
	Top  *cardView `json:"top,omitempty"`
}

// solitaireView is a board as its player may see it.
type solitaireView struct {
	Tier klondike.Tier `json:"tier"`

	Stock  int            `json:"stock"` // how many cards are left in it, not which
	Waste  []cardView     `json:"waste"` // the top few, in the order they were turned
	WasteN int            `json:"waste_n"`
	Table  []solPileView  `json:"table"`
	Found  []solFoundView `json:"found"`

	Passes  int  `json:"passes"` // through the stock, counting this one; -1 unlimited
	Moves   int  `json:"moves"`
	CanAuto bool `json:"can_auto"`
	Won     bool `json:"won"` // every card face up, stock and waste empty: one press finishes it

	Home      int     `json:"home"`       // cards on the foundations
	PerCard   float64 `json:"per_card"`   // what one more is worth
	BreakEven int     `json:"break_even"` // how many gets you square with the house
	Bet       int64   `json:"bet"`
	Stands    int64   `json:"stands"` // what cashing out right now actually pays

	Phase   string `json:"phase"`
	Outcome string `json:"outcome,omitempty"`
	Payout  int64  `json:"payout,omitempty"`
	Rake    int64  `json:"rake,omitempty"`
	Net     int64  `json:"net"`
}

// wasteShown is how much of the waste the felt fans out. Three, because that is
// what a three-card draw puts down and the rest of the pile is just a pile.
const wasteShown = 3

func viewSolitaire(g klondike.State) solitaireView {
	v := solitaireView{
		Tier:      g.Tier,
		Stock:     len(g.Stock),
		WasteN:    len(g.Waste),
		Passes:    g.PassesLeft(),
		Moves:     g.Moves,
		CanAuto:   g.CanAuto(),
		Won:       g.Won(),
		Home:      g.Home(),
		PerCard:   g.PerCard(),
		BreakEven: g.Tier.BreakEven(),
		Bet:       g.Bet,
		// What cashing out right now would actually land on the stack, rake already
		// out of it. The pre-rake figure would have the felt advertising a number
		// the house doesn't hand over.
		Stands:  g.Pays(),
		Phase:   string(g.Phase),
		Outcome: string(g.Outcome),
		Payout:  g.Payout,
		Rake:    g.Rake,
		Net:     g.Net(),
	}

	from := len(g.Waste) - wasteShown
	if from < 0 {
		from = 0
	}
	for _, c := range g.Waste[from:] {
		v.Waste = append(v.Waste, viewCard(c))
	}

	v.Table = make([]solPileView, klondike.Piles)
	for i, p := range g.Table {
		v.Table[i] = solPileView{Down: len(p.Down)}
		for _, c := range p.Up {
			v.Table[i].Up = append(v.Table[i].Up, viewCard(c))
		}
	}

	v.Found = make([]solFoundView, klondike.Foundations)
	for i, f := range g.Found {
		suit := cards.Suit(i)
		fv := solFoundView{Suit: suit.String(), Red: suit == cards.Hearts || suit == cards.Diamonds, N: len(f)}
		if len(f) > 0 {
			top := viewCard(f[len(f)-1])
			fv.Top = &top
		}
		v.Found[i] = fv
	}
	return v
}

// solEventView is one thing the table animates. See the note at the top: unlike
// blackjack's, these need nothing stripped out of them.
type solEventView struct {
	Kind  string     `json:"kind"`
	Cards []cardView `json:"cards,omitempty"`
	From  string     `json:"from,omitempty"`
	To    string     `json:"to,omitempty"`
	Home  int        `json:"home"`
	Pays  int64      `json:"pays"`
}

func viewSolEvents(evs []klondike.Event) []solEventView {
	out := make([]solEventView, 0, len(evs))
	for _, e := range evs {
		v := solEventView{Kind: e.Kind, From: e.From, To: e.To, Home: e.Home, Pays: e.Pays}
		for _, c := range e.Cards {
			v.Cards = append(v.Cards, viewCard(c))
		}
		out = append(out, v)
	}
	return out
}

// handleSolitaireStart takes the stake and deals the board. Same order as a
// blackjack deal: the chips are staked first, in the same statement that checks
// they exist, so two starts fired at once cannot buy the same deck twice.
func (s *Server) handleSolitaireStart(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 := klondike.TierBySlug(req.Tier)
	if err != nil {
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "pick a deal"})
		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 deck"})
			return
		}
		slog.Error("games: solitaire stake", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

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

// solitaireErrors are the illegal moves a player makes by playing, rather than
// by tampering. Each gets said back to them in words, because "that move isn't
// legal" over a board with 60 legal-looking targets on it is not an answer.
var solitaireErrors = map[error]string{
	klondike.ErrWontGo:       "that card doesn't go there",
	klondike.ErrNotASequence: "you can only lift a run that goes down in rank and alternates colour",
	klondike.ErrEmptyPile:    "there's nothing there",
	klondike.ErrNoDraw:       "the stock is empty",
	klondike.ErrNoPasses:     "that was your last pass through the stock",
	klondike.ErrNothingHome:  "nothing can go home right now",
	klondike.ErrGameOver:     "that board is finished",
}

// handleSolitaireMove plays one move: a draw, a card moved, a card sent home, an
// auto-finish, or cashing the board in.
func (s *Server) handleSolitaireMove(w http.ResponseWriter, r *http.Request) {
	user, ok := s.player(w, r)
	if !ok {
		return
	}
	var move klondike.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: solitaire load", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	if live.Game != gameSolitaire {
		writeJSONStatus(w, http.StatusConflict, map[string]string{"error": "finish the hand you're in first"})
		return
	}
	var g klondike.State
	if err := json.Unmarshal(live.State, &g); err != nil {
		slog.Error("games: unreadable solitaire board", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	next, evs, err := klondike.ApplyMove(g, move)
	if err != nil {
		msg, known := solitaireErrors[err]
		if !known {
			msg = "that move isn't legal here"
		}
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": msg})
		return
	}
	s.persistSolitaire(w, user, next, evs, live.Seed1, live.Seed2, false)
}

// persistSolitaire writes the board back and answers the browser.
func (s *Server) persistSolitaire(w http.ResponseWriter, user string, g klondike.State, evs []klondike.Event, seed1, seed2 uint64, fresh bool) {
	blob, err := json.Marshal(g)
	if err != nil {
		slog.Error("games: marshal solitaire", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	done := g.Phase == klondike.PhaseDone
	v, ok := s.commit(w, user, finished{
		Game: gameSolitaire, 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 board is gone from storage, so the table has none to show — but
	// the browser still needs the final one to animate the last cards onto.
	if done {
		sv := viewSolitaire(g)
		v.Solitaire = &sv
	}
	v.SolEvents = viewSolEvents(evs)
	writeJSON(w, v)
}
