package web

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

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

// Hangman, played for chips.
//
// The same shape as the blackjack table: the browser sends intents, the server
// holds the state, and the payload carries only what the player is entitled to
// see. Here that means the *masked* phrase. The unmasked one is in the engine
// state, which is in game_live_hands, which is on this side of the wire — a
// phrase sent down and flagged hidden is a phrase read out of devtools, and the
// game would be a formality.

// cellView is one position in the phrase, as the browser draws it.
//
// Ch is empty while the letter is hidden — not the letter with a flag beside
// it. Slot says whether this is a position you'd guess at all: a space or an
// exclamation mark is scaffolding, shows from the start, and gets no tile.
type cellView struct {
	Ch   string `json:"ch"`
	Slot bool   `json:"slot"`
}

// hangmanView is a game as its player may see it.
type hangmanView struct {
	Tier     hangman.Tier `json:"tier"`
	Cells    []cellView   `json:"cells"`
	Tried    []string     `json:"tried"` // every letter guessed, right or wrong
	Wrong    []string     `json:"wrong"` // just the misses — the gallows counts these
	Lives    int          `json:"lives"`
	MaxWrong int          `json:"max_wrong"`
	Multiple float64      `json:"multiple"` // what a win is worth right now
	Bet      int64        `json:"bet"`
	Stands   int64        `json:"stands"` // what the player would actually be paid if they won now

	Phase   string `json:"phase"`
	Outcome string `json:"outcome,omitempty"`
	Phrase  string `json:"phrase,omitempty"` // only once it's over
	Payout  int64  `json:"payout,omitempty"`
	Rake    int64  `json:"rake,omitempty"`
	Net     int64  `json:"net"`
}

func viewHangman(g hangman.State) hangmanView {
	v := hangmanView{
		Tier:     g.Tier,
		Lives:    g.Lives(),
		MaxWrong: hangman.MaxWrong,
		Multiple: g.Multiple(),
		Bet:      g.Bet,
		// What the player would actually collect, rake already taken out. Quoting
		// the pre-rake figure here would have the felt advertising a payout 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(),
	}
	for i, r := range g.Runes {
		c := cellView{Slot: hangman.Guessable(r)}
		if i < len(g.Shown) && g.Shown[i] {
			c.Ch = string(r)
		}
		v.Cells = append(v.Cells, c)
	}
	for _, r := range g.Tried {
		v.Tried = append(v.Tried, string(r))
	}
	for _, r := range g.Wrong {
		v.Wrong = append(v.Wrong, string(r))
	}
	// The phrase goes over the wire exactly once: when the game is over and it no
	// longer decides anything.
	if g.Phase == hangman.PhaseDone {
		v.Phrase = g.Phrase
	}
	return v
}

// handleHangmanStart takes the bet and draws a phrase. Same order as a deal:
// the chips are staked first, in the same statement that checks they exist, so
// two starts fired at once cannot bet the same chip.
func (s *Server) handleHangmanStart(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 := hangman.TierBySlug(req.Tier)
	if err != nil {
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": "pick a length"})
		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: hangman 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 := hangman.New(req.Bet, tier, blackjack.DefaultRules().RakePct, rng)
	if err != nil {
		// The game never happened, so the stake never should have left.
		_ = storage.Award(user, req.Bet)
		slog.Error("games: hangman start", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	s.persistHangman(w, user, g, evs, seed1, seed2, true)
}

// handleHangmanGuess plays one guess: a letter, or the whole phrase.
func (s *Server) handleHangmanGuess(w http.ResponseWriter, r *http.Request) {
	user, ok := s.player(w, r)
	if !ok {
		return
	}
	var move hangman.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: hangman load", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	if live.Game != gameHangman {
		writeJSONStatus(w, http.StatusConflict, map[string]string{"error": "finish the hand you're in first"})
		return
	}
	var g hangman.State
	if err := json.Unmarshal(live.State, &g); err != nil {
		slog.Error("games: unreadable hangman game", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	next, evs, err := hangman.ApplyMove(g, move)
	if err != nil {
		// A letter already tried is the one illegal move a player makes by
		// accident rather than by trying it on, so it gets its own answer.
		msg := "that guess isn't legal here"
		if errors.Is(err, hangman.ErrAlreadyTried) {
			msg = "you've already tried that one"
		}
		writeJSONStatus(w, http.StatusBadRequest, map[string]string{"error": msg})
		return
	}
	s.persistHangman(w, user, next, evs, live.Seed1, live.Seed2, false)
}

// persistHangman writes the game back and answers the browser.
func (s *Server) persistHangman(w http.ResponseWriter, user string, g hangman.State, evs []hangman.Event, seed1, seed2 uint64, fresh bool) {
	blob, err := json.Marshal(g)
	if err != nil {
		slog.Error("games: marshal hangman", "user", user, "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	done := g.Phase == hangman.PhaseDone
	v, ok := s.commit(w, user, finished{
		Game: gameHangman, 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 reveal the phrase onto.
	if done {
		hv := viewHangman(g)
		v.Hangman = &hv
	}
	v.HangEvents = evs
	writeJSON(w, v)
}
