package main

import "math/rand"

// Бот Техасского Холдема — честного решателя GTO (оптимальной по
// теории игр стратегии) здесь нет и быть не может в разумном
// объёме кода; вместо этого используется оценка силы руки методом
// Монте-Карло (симуляция множества случайных завершений раздачи
// против случайных карт соперников) в сочетании с элементарным
// расчётом шансов банка. Это не идеальная игра — блеф и адаптация к
// стилю соперника учитываются лишь в грубом приближении — но
// решения основаны на честной оценке вероятности выигрыша, а не на
// произвольных правилах.

// PokerDifficulty — уровень сложности бота. Игроку выбирать
// сложность НЕ предлагается — при создании партии каждому боту
// случайно назначается один из трёх уровней (см. NewPokerModel).
type PokerDifficulty int

const (
	PokerDifficultyEasy PokerDifficulty = iota
	PokerDifficultyMedium
	PokerDifficultyHard
)

// PokerBot — бот-соперник за одним из мест за столом.
type PokerBot struct {
	Difficulty PokerDifficulty
}

// remainingDeck возвращает все карты колоды, ещё не известные как
// использованные (не в своей руке и не на столе) — для честной
// симуляции возможных карт соперников и оставшейся части борда.
func remainingDeck(hole [2]Card, board []Card) []Card {
	used := map[Card]bool{hole[0]: true, hole[1]: true}
	for _, c := range board {
		used[c] = true
	}
	out := make([]Card, 0, 52-len(used))
	for s := Clubs; s <= Spades; s++ {
		for r := Ace; r <= King; r++ {
			card := Card{Suit: s, Rank: r}
			if !used[card] {
				out = append(out, card)
			}
		}
	}
	return out
}

// pokerHandStrength оценивает вероятность выигрыша (или разделения
// банка — считается за половину) руки hole на текущем борде board
// против numOpponents случайных соперников, симулируя iterations
// случайных завершений раздачи.
func pokerHandStrength(hole [2]Card, board []Card, numOpponents, iterations int, rnd *rand.Rand) float64 {
	if numOpponents <= 0 {
		return 1
	}
	wins := 0.0
	for i := 0; i < iterations; i++ {
		deck := remainingDeck(hole, board)
		rnd.Shuffle(len(deck), func(a, b int) { deck[a], deck[b] = deck[b], deck[a] })

		pos := 0
		oppHoles := make([][2]Card, numOpponents)
		for o := 0; o < numOpponents; o++ {
			oppHoles[o] = [2]Card{deck[pos], deck[pos+1]}
			pos += 2
		}
		fullBoard := append([]Card{}, board...)
		for len(fullBoard) < 5 {
			fullBoard = append(fullBoard, deck[pos])
			pos++
		}

		myValue := EvaluateBest5(append([]Card{hole[0], hole[1]}, fullBoard...))
		winning, tie := true, false
		for _, oh := range oppHoles {
			ov := EvaluateBest5(append([]Card{oh[0], oh[1]}, fullBoard...))
			switch {
			case ComparePokerHands(ov, myValue) > 0:
				winning = false
			case ComparePokerHands(ov, myValue) == 0:
				tie = true
			}
			if !winning {
				break
			}
		}
		if winning {
			if tie {
				wins += 0.5
			} else {
				wins += 1.0
			}
		}
	}
	return wins / float64(iterations)
}

// crudeStrength — грубая, не основанная на симуляции оценка силы
// руки для лёгкого уровня: до флопа — простая таблица по паре и
// старшинству карт, после флопа — приблизительная оценка по
// категории собранной комбинации (без учёта кикеров).
func crudeStrength(hole [2]Card, board []Card) float64 {
	if len(board) == 0 {
		r1, r2 := pokerRankValue(hole[0].Rank), pokerRankValue(hole[1].Rank)
		if r1 == r2 {
			return 0.5 + float64(r1)/28.0
		}
		high, low := r1, r2
		if low > high {
			high, low = low, high
		}
		strength := float64(high)/28.0 + float64(low)/56.0
		if hole[0].Suit == hole[1].Suit {
			strength += 0.05
		}
		if high-low <= 3 {
			strength += 0.03
		}
		return strength
	}
	all := append([]Card{hole[0], hole[1]}, board...)
	v := EvaluateBest5(all)
	return 0.3 + float64(v.Category)/8.0*0.6
}

// potSize — сколько всего фишек уже внесено в банк за эту раздачу
// (по всем прошедшим раундам торгов вместе).
func (g *PokerGameState) potSize() int {
	total := 0
	for _, p := range g.Players {
		total += p.TotalContributed
	}
	return total
}

// countOpponentsInHand — сколько соперников (кроме seatIdx) всё ещё
// не сбросили карты в этой раздаче.
func (g *PokerGameState) countOpponentsInHand(seatIdx int) int {
	n := 0
	for i, p := range g.Players {
		if i != seatIdx && !p.Folded && !p.Eliminated {
			n++
		}
	}
	return n
}

// raiseTotalForStrength считает суммарную ставку раунда для рейза,
// примерно пропорциональную банку и силе руки (от половины банка до
// полного банка), с учётом ограничений минимального рейза и
// собственного стека.
func raiseTotalForStrength(g *PokerGameState, seatIdx int, strength float64) int {
	p := g.Players[seatIdx]
	pot := g.potSize()
	raiseSize := int(float64(pot) * (0.5 + strength*0.5))
	if raiseSize < g.BigBlind {
		raiseSize = g.BigBlind
	}
	total := g.highestBet() + raiseSize

	maxTotal := p.CurrentBet + p.Stack
	if total > maxTotal {
		total = maxTotal
	}
	minTotal := g.highestBet() + g.BigBlind
	if g.highestBet() == 0 {
		minTotal = g.BigBlind
	}
	if total < minTotal {
		total = minTotal
	}
	if total > maxTotal {
		total = maxTotal
	}
	return total
}

// PokerBotAction — решение бота на один ход.
type PokerBotAction struct {
	Kind      string // "fold", "check", "call", "bet"
	BetAmount int    // суммарная ставка раунда — только для "bet"
}

// DecideAction выбирает действие бота на его ходу seatIdx согласно
// уровню сложности.
func (b PokerBot) DecideAction(g *PokerGameState, seatIdx int, rnd *rand.Rand) PokerBotAction {
	p := g.Players[seatIdx]
	toCall := g.highestBet() - p.CurrentBet

	if b.Difficulty == PokerDifficultyEasy {
		return b.decideEasy(g, seatIdx, toCall)
	}
	return b.decideByStrength(g, seatIdx, toCall, rnd)
}

func (b PokerBot) decideEasy(g *PokerGameState, seatIdx int, toCall int) PokerBotAction {
	p := g.Players[seatIdx]
	strength := crudeStrength(p.HoleCards, g.Board)

	if toCall == 0 {
		if strength > 0.62 {
			return PokerBotAction{Kind: "bet", BetAmount: raiseTotalForStrength(g, seatIdx, strength)}
		}
		return PokerBotAction{Kind: "check"}
	}
	callOdds := float64(toCall) / float64(g.potSize()+toCall)
	if strength+0.25 >= callOdds || toCall <= g.BigBlind*2 {
		return PokerBotAction{Kind: "call"}
	}
	return PokerBotAction{Kind: "fold"}
}

func (b PokerBot) decideByStrength(g *PokerGameState, seatIdx int, toCall int, rnd *rand.Rand) PokerBotAction {
	p := g.Players[seatIdx]
	iterations := 80
	bluffChance := 0.0
	aggression := 1.0
	if b.Difficulty == PokerDifficultyHard {
		iterations = 200
		bluffChance = 0.1
		aggression = 1.15
	}

	if bluffChance > 0 && rnd.Float64() < bluffChance {
		return PokerBotAction{Kind: "bet", BetAmount: raiseTotalForStrength(g, seatIdx, 0.8)}
	}

	strength := pokerHandStrength(p.HoleCards, g.Board, g.countOpponentsInHand(seatIdx), iterations, rnd)

	if toCall == 0 {
		if strength > 0.65*aggression {
			return PokerBotAction{Kind: "bet", BetAmount: raiseTotalForStrength(g, seatIdx, strength)}
		}
		return PokerBotAction{Kind: "check"}
	}

	potOddsNeeded := float64(toCall) / float64(g.potSize()+toCall)
	if strength < potOddsNeeded {
		return PokerBotAction{Kind: "fold"}
	}
	if strength > 0.75*aggression {
		return PokerBotAction{Kind: "bet", BetAmount: raiseTotalForStrength(g, seatIdx, strength)}
	}
	return PokerBotAction{Kind: "call"}
}

// PlayFullTurn исполняет один ход бота на его месте seatIdx.
func (b PokerBot) PlayFullTurn(g *PokerGameState, seatIdx int, rnd *rand.Rand) error {
	action := b.DecideAction(g, seatIdx, rnd)
	switch action.Kind {
	case "fold":
		return g.Fold()
	case "check":
		return g.Check()
	case "call":
		return g.Call()
	case "bet":
		return g.Bet(action.BetAmount)
	}
	return nil
}
