package main

import (
	"errors"
	"math/rand"
	"sort"
)

// Техасский Холдем (No-Limit) — турнирный формат с выбыванием: игра
// идёт до тех пор, пока не останется один игрок с фишками. Блайнды
// фиксированы (не растут по ходу турнира) — это сознательное
// упрощение ради управляемого объёма игры.
//
// ВАЖНОЕ УПРОЩЕНИЕ ПРАВИЛ: короткий олл-ин рейзом МЕНЬШЕ полного
// размера предыдущего рейза, по строгим правилам, не должен заново
// открывать торги игрокам, которые уже походили в этом раунде (они
// обязаны лишь доколлировать либо сбросить, но не имеют права
// повторно рейзить в ответ именно на такой недостаточный рейз). Эта
// игра такого разграничения не делает — любой рейз (в том числе
// короткий олл-ин) заново даёт всем активным игрокам право хода,
// включая повторный рейз. Расхождение с турнирными правилами
// возможно только в редких ситуациях с несколькими короткостёчными
// олл-инами подряд.

// PokerPhase — фаза текущей раздачи.
type PokerPhase int

const (
	PokerPhasePreflop PokerPhase = iota
	PokerPhaseFlop
	PokerPhaseTurn
	PokerPhaseRiver
	PokerPhaseShowdown       // раздача завершена, идёт показ карт/распределение банка
	PokerPhaseTournamentOver // остался один игрок с фишками — турнир окончен
)

// PokerPlayer — один участник партии.
type PokerPlayer struct {
	Name  string
	Stack int // фишки, НЕ поставленные в текущий банк
	Bot   *PokerBot

	HoleCards  [2]Card
	Folded     bool
	AllIn      bool
	Eliminated bool // стек обнулился — выбыл из турнира насовсем

	CurrentBet       int // ставка ТЕКУЩЕГО раунда торгов (preflop/flop/turn/river)
	TotalContributed int // сколько всего внесено в банк за ВСЮ раздачу (для сайд-потов)
}

// PokerSidePot — один "слой" банка с собственным списком, кто может
// на него претендовать (см. computeSidePots).
type PokerSidePot struct {
	Amount          int
	EligiblePlayers []int
}

// PokerHandResult — итог завершённой раздачи (для отображения на
// экране шоудауна/сброса).
type PokerHandResult struct {
	Pots           []PokerSidePot
	Winners        map[int]int // индекс игрока -> сколько выиграл всего по всем банкам
	WentToShowdown bool
	HandValues     map[int]PokerHandValue // только для тех, кто дошёл до шоудауна
}

// PokerGameState — состояние всей партии (турнира).
type PokerGameState struct {
	Players []*PokerPlayer
	Deck    []Card
	Board   []Card

	DealerIdx  int
	SmallBlind int
	BigBlind   int

	Phase            PokerPhase
	CurrentPlayerIdx int

	actedThisRound   map[int]bool // кто уже отреагировал на ТЕКУЩИЙ уровень ставки в этом раунде торгов
	lastAggressorIdx int          // кто последний раз ставил/рейзил в этом раунде (-1, если раунд ещё без ставок)

	Result *PokerHandResult
}

var (
	ErrPokerWrongPhase     = errors.New("недопустимое действие для текущей фазы")
	ErrPokerNotYourTurn    = errors.New("сейчас не ваш ход")
	ErrPokerCannotCheck    = errors.New("нельзя пасовать (чек) — на столе уже есть ставка, которую нужно уравнять или сбросить")
	ErrPokerBetTooLow      = errors.New("ставка меньше минимально допустимой")
	ErrPokerNotEnoughChips = errors.New("недостаточно фишек для такой ставки")
)

// NewPokerGame создаёт новый турнир: заданное число игроков (первый
// — человек), фиксированный стартовый стек и блайнды.
func NewPokerGame(numPlayers, startingStack, smallBlind, bigBlind int, rnd *rand.Rand) *PokerGameState {
	g := &PokerGameState{SmallBlind: smallBlind, BigBlind: bigBlind, DealerIdx: -1}
	for i := 0; i < numPlayers; i++ {
		g.Players = append(g.Players, &PokerPlayer{Stack: startingStack})
	}
	g.startNewHand(rnd)
	return g
}

// activeTournamentPlayers — индексы игроков, ещё не выбывших из
// турнира.
func (g *PokerGameState) activeTournamentPlayers() []int {
	var out []int
	for i, p := range g.Players {
		if !p.Eliminated {
			out = append(out, i)
		}
	}
	return out
}

// nextActiveTournamentSeat находит следующее по кругу от from место
// среди ещё не выбывших из турнира игроков.
func (g *PokerGameState) nextActiveTournamentSeat(from int) int {
	n := len(g.Players)
	for i := 1; i <= n; i++ {
		idx := (from + i) % n
		if !g.Players[idx].Eliminated {
			return idx
		}
	}
	return from
}

// nextToAct находит следующего игрока по кругу от from, который ещё
// может действовать в текущем раунде торгов (не сбросил, не в
// олл-ине, не выбыл).
func (g *PokerGameState) nextToAct(from int) int {
	n := len(g.Players)
	for i := 1; i <= n; i++ {
		idx := (from + i) % n
		p := g.Players[idx]
		if !p.Folded && !p.AllIn && !p.Eliminated {
			return idx
		}
	}
	return from
}

func (g *PokerGameState) countActingPlayers() int {
	n := 0
	for _, p := range g.Players {
		if !p.Folded && !p.AllIn && !p.Eliminated {
			n++
		}
	}
	return n
}

func (g *PokerGameState) countInHand() int {
	n := 0
	for _, p := range g.Players {
		if !p.Folded && !p.Eliminated {
			n++
		}
	}
	return n
}

// startNewHand начинает новую раздачу: сдвигает дилера, раздаёт
// карты, ставит блайнды, определяет первый ход.
func (g *PokerGameState) startNewHand(rnd *rand.Rand) {
	active := g.activeTournamentPlayers()
	if len(active) <= 1 {
		g.Phase = PokerPhaseTournamentOver
		return
	}

	for _, p := range g.Players {
		p.HoleCards = [2]Card{}
		p.Folded = p.Eliminated
		p.AllIn = false
		p.CurrentBet = 0
		p.TotalContributed = 0
	}
	g.Result = nil
	g.Board = nil

	g.Deck = shuffledDeck(rnd)

	if g.DealerIdx < 0 {
		g.DealerIdx = active[0]
	} else {
		g.DealerIdx = g.nextActiveTournamentSeat(g.DealerIdx)
	}

	// В игре один на один (heads-up) дилер ставит МАЛЫЙ блайнд сам
	// (а не "следующий" игрок) — общее правило "SB = следующий после
	// дилера" здесь даёт неверный результат, так как с двумя
	// игроками "следующий после дилера" и "следующий после этого
	// игрока" зацикливаются друг на друга, меняя роли блайндов местами.
	var sbIdx, bbIdx int
	if len(active) == 2 {
		sbIdx = g.DealerIdx
		bbIdx = g.nextActiveTournamentSeat(g.DealerIdx)
	} else {
		sbIdx = g.nextActiveTournamentSeat(g.DealerIdx)
		bbIdx = g.nextActiveTournamentSeat(sbIdx)
	}
	g.postBlind(sbIdx, g.SmallBlind)
	g.postBlind(bbIdx, g.BigBlind)

	for i := 0; i < 2; i++ {
		for _, idx := range g.activeTournamentPlayers() {
			g.Players[idx].HoleCards[i] = g.drawCard()
		}
	}

	g.Phase = PokerPhasePreflop
	g.lastAggressorIdx = bbIdx
	g.actedThisRound = map[int]bool{}
	g.CurrentPlayerIdx = g.nextToAct(bbIdx)
}

func (g *PokerGameState) postBlind(idx, amount int) {
	p := g.Players[idx]
	if amount >= p.Stack {
		amount = p.Stack
		p.AllIn = true
	}
	p.Stack -= amount
	p.CurrentBet = amount
	p.TotalContributed = amount
}

func (g *PokerGameState) drawCard() Card {
	card := g.Deck[0]
	g.Deck = g.Deck[1:]
	return card
}

func shuffledDeck(rnd *rand.Rand) []Card {
	deck := make([]Card, 0, 52)
	for s := Clubs; s <= Spades; s++ {
		for r := Ace; r <= King; r++ {
			deck = append(deck, Card{Suit: s, Rank: r})
		}
	}
	rnd.Shuffle(len(deck), func(i, j int) { deck[i], deck[j] = deck[j], deck[i] })
	return deck
}

// highestBet — самая большая ставка ЭТОГО раунда торгов среди всех
// игроков.
func (g *PokerGameState) highestBet() int {
	max := 0
	for _, p := range g.Players {
		if p.CurrentBet > max {
			max = p.CurrentBet
		}
	}
	return max
}

// Fold — игрок сбрасывает карты.
func (g *PokerGameState) Fold() error {
	if err := g.checkCanAct(); err != nil {
		return err
	}
	g.Players[g.CurrentPlayerIdx].Folded = true
	g.advanceAfterAction()
	return nil
}

// Check — пропустить ход без ставки (легально только если нечего уравнивать).
func (g *PokerGameState) Check() error {
	if err := g.checkCanAct(); err != nil {
		return err
	}
	p := g.Players[g.CurrentPlayerIdx]
	if p.CurrentBet != g.highestBet() {
		return ErrPokerCannotCheck
	}
	g.advanceAfterAction()
	return nil
}

// Call — уравнять текущую наибольшую ставку (либо пойти в олл-ин,
// если фишек не хватает на полное уравнивание).
func (g *PokerGameState) Call() error {
	if err := g.checkCanAct(); err != nil {
		return err
	}
	p := g.Players[g.CurrentPlayerIdx]
	need := g.highestBet() - p.CurrentBet
	if need <= 0 {
		return g.Check()
	}
	if need >= p.Stack {
		need = p.Stack
		p.AllIn = true
	}
	p.Stack -= need
	p.CurrentBet += need
	p.TotalContributed += need
	g.advanceAfterAction()
	return nil
}

// Bet ставит (или рейзит) так, чтобы СУММАРНАЯ ставка игрока в этом
// раунде торгов стала равна totalAmount. totalAmount должен быть не
// меньше текущей наибольшей ставки плюс большой блайнд (минимальный
// рейз в этой упрощённой реализации — см. пояснение в начале файла).
func (g *PokerGameState) Bet(totalAmount int) error {
	if err := g.checkCanAct(); err != nil {
		return err
	}
	p := g.Players[g.CurrentPlayerIdx]
	minTotal := g.highestBet() + g.BigBlind
	if g.highestBet() == 0 {
		minTotal = g.BigBlind
	}
	maxTotal := p.CurrentBet + p.Stack
	if totalAmount > maxTotal {
		return ErrPokerNotEnoughChips
	}
	if totalAmount < minTotal && totalAmount < maxTotal {
		return ErrPokerBetTooLow
	}
	delta := totalAmount - p.CurrentBet
	p.Stack -= delta
	p.CurrentBet = totalAmount
	p.TotalContributed += delta
	if p.Stack == 0 {
		p.AllIn = true
	}
	g.lastAggressorIdx = g.CurrentPlayerIdx
	g.actedThisRound = map[int]bool{g.CurrentPlayerIdx: true}
	g.advanceAfterAction()
	return nil
}

// AllIn ставит весь оставшийся стек.
func (g *PokerGameState) AllIn() error {
	if err := g.checkCanAct(); err != nil {
		return err
	}
	p := g.Players[g.CurrentPlayerIdx]
	return g.Bet(p.CurrentBet + p.Stack)
}

func (g *PokerGameState) checkCanAct() error {
	if g.Phase == PokerPhaseShowdown || g.Phase == PokerPhaseTournamentOver {
		return ErrPokerWrongPhase
	}
	p := g.Players[g.CurrentPlayerIdx]
	if p.Folded || p.AllIn || p.Eliminated {
		return ErrPokerNotYourTurn
	}
	return nil
}

// advanceAfterAction обрабатывает переход хода после действия
// текущего игрока: завершение раздачи (остался один не сбросивший),
// переход к следующей фазе (когда раунд торгов завершён), либо
// передачу хода следующему активному игроку.
func (g *PokerGameState) advanceAfterAction() {
	if g.countInHand() == 1 {
		g.resolveHandByFold()
		return
	}

	if g.actedThisRound == nil {
		g.actedThisRound = map[int]bool{}
	}
	g.actedThisRound[g.CurrentPlayerIdx] = true

	allActed := true
	for i, p := range g.Players {
		if !p.Folded && !p.AllIn && !p.Eliminated && !g.actedThisRound[i] {
			allActed = false
			break
		}
	}
	if allActed {
		g.advancePhase()
		return
	}
	g.CurrentPlayerIdx = g.nextToAct(g.CurrentPlayerIdx)
}

// advancePhase переходит к следующей фазе раздачи (флоп/тёрн/ривер/
// шоудаун), сбрасывая ставки раунда. Если активных (способных
// действовать) игроков не осталось вовсе (все, кроме максимум
// одного, в олл-ине) — карты дораздаются сразу до шоудауна без
// дальнейших торгов.
func (g *PokerGameState) advancePhase() {
	for _, p := range g.Players {
		p.CurrentBet = 0
	}
	g.actedThisRound = map[int]bool{}

	switch g.Phase {
	case PokerPhasePreflop:
		g.Board = append(g.Board, g.drawCard(), g.drawCard(), g.drawCard())
		g.Phase = PokerPhaseFlop
	case PokerPhaseFlop:
		g.Board = append(g.Board, g.drawCard())
		g.Phase = PokerPhaseTurn
	case PokerPhaseTurn:
		g.Board = append(g.Board, g.drawCard())
		g.Phase = PokerPhaseRiver
	case PokerPhaseRiver:
		g.resolveShowdown()
		return
	}

	if g.countActingPlayers() <= 1 {
		// все, кроме максимум одного, в олл-ине — дальше действовать
		// некому, доигрываем карты до шоудауна автоматически
		g.advancePhase()
		return
	}
	g.lastAggressorIdx = -1
	g.CurrentPlayerIdx = g.nextToAct(g.DealerIdx)
}

// resolveHandByFold завершает раздачу, когда все, кроме одного,
// сбросили карты — оставшийся игрок забирает весь банк без показа карт.
func (g *PokerGameState) resolveHandByFold() {
	var winnerIdx int
	for i, p := range g.Players {
		if !p.Folded && !p.Eliminated {
			winnerIdx = i
			break
		}
	}
	pots := computeSidePots(g.Players)
	total := 0
	for _, pot := range pots {
		total += pot.Amount
	}
	g.Players[winnerIdx].Stack += total
	g.Result = &PokerHandResult{
		Pots:           pots,
		Winners:        map[int]int{winnerIdx: total},
		WentToShowdown: false,
	}
	g.Phase = PokerPhaseShowdown
	g.markEliminated()
}

// resolveShowdown вскрывает карты и распределяет все банки (включая
// сайд-поты) между лучшими руками среди дошедших до конца.
func (g *PokerGameState) resolveShowdown() {
	pots := computeSidePots(g.Players)
	values := map[int]PokerHandValue{}
	for i, p := range g.Players {
		if !p.Folded && !p.Eliminated {
			all := append([]Card{p.HoleCards[0], p.HoleCards[1]}, g.Board...)
			values[i] = EvaluateBest5(all)
		}
	}

	winnings := map[int]int{}
	for _, pot := range pots {
		var best []int
		for _, idx := range pot.EligiblePlayers {
			if len(best) == 0 {
				best = []int{idx}
				continue
			}
			cmp := ComparePokerHands(values[idx], values[best[0]])
			if cmp > 0 {
				best = []int{idx}
			} else if cmp == 0 {
				best = append(best, idx)
			}
		}
		share := pot.Amount / len(best)
		remainder := pot.Amount - share*len(best)
		for i, idx := range best {
			amt := share
			if i == 0 {
				amt += remainder // остаток от неровного деления — первому по очереди из победителей
			}
			winnings[idx] += amt
			g.Players[idx].Stack += amt
		}
	}

	g.Result = &PokerHandResult{
		Pots:           pots,
		Winners:        winnings,
		WentToShowdown: true,
		HandValues:     values,
	}
	g.Phase = PokerPhaseShowdown
	g.markEliminated()
}

func (g *PokerGameState) markEliminated() {
	for _, p := range g.Players {
		if p.Stack == 0 && !p.Eliminated {
			p.Eliminated = true
		}
	}
}

// NextHand начинает следующую раздачу турнира (или переводит в
// PokerPhaseTournamentOver, если остался один игрок с фишками).
func (g *PokerGameState) NextHand(rnd *rand.Rand) {
	if g.Phase != PokerPhaseShowdown {
		return
	}
	g.startNewHand(rnd)
}

// computeSidePots раскладывает общий банк на "слои" по уровням
// взносов игроков за раздачу — стандартный алгоритм сайд-потов:
// каждый слой формируют все, кто внёс хотя бы до этого уровня
// (включая уже сбросивших — их фишки остаются в банке), а
// претендовать на слой могут только НЕ сбросившие игроки, дошедшие
// до этого уровня.
func computeSidePots(players []*PokerPlayer) []PokerSidePot {
	levelSet := map[int]bool{}
	for _, p := range players {
		if p.TotalContributed > 0 {
			levelSet[p.TotalContributed] = true
		}
	}
	var levels []int
	for lvl := range levelSet {
		levels = append(levels, lvl)
	}
	sort.Ints(levels)

	var pots []PokerSidePot
	prev := 0
	carry := 0
	for _, level := range levels {
		layerSize := level - prev
		contributors := 0
		var eligible []int
		for i, p := range players {
			if p.TotalContributed >= level {
				contributors++
				if !p.Folded {
					eligible = append(eligible, i)
				}
			}
		}
		amount := layerSize*contributors + carry
		if len(eligible) == 0 {
			carry = amount
		} else {
			pots = append(pots, PokerSidePot{Amount: amount, EligiblePlayers: eligible})
			carry = 0
		}
		prev = level
	}
	if carry > 0 && len(pots) > 0 {
		pots[len(pots)-1].Amount += carry
	}
	return pots
}
