package main

import (
	"errors"
	"fmt"
)

// Phase — фаза текущего хода игрока.
type Phase int

const (
	// PhaseDraw — игрок должен взять карту (из колоды или сброса)
	// либо объявить Тонк, если это позволяет вес его руки.
	PhaseDraw Phase = iota
	// PhaseAction — игрок уже взял карту; может выкладывать
	// комбинации, подкладывать карты к столу и в конце обязан
	// сбросить одну карту.
	PhaseAction
	// PhaseGameOver — раунд завершён.
	PhaseGameOver
)

// MeldKind различает тип выложенной на стол комбинации.
type MeldKind int

const (
	KindSet MeldKind = iota
	KindSequence
)

// TableMeld — комбинация, выложенная на стол. Владелец фиксируется
// тем, кто её впервые выложил, но подкладывать карты может любой
// игрок.
type TableMeld struct {
	Owner int
	Kind  MeldKind
	Cards []Card
}

var (
	ErrWrongPhase       = errors.New("недопустимое действие для текущей фазы хода")
	ErrDeckExhausted    = errors.New("колода и сброс исчерпаны — раунд закончен вничью")
	ErrInvalidMeld      = errors.New("карты не образуют валидную комбинацию")
	ErrCardNotInHand    = errors.New("карты нет на руке игрока")
	ErrMeldNotFound     = errors.New("комбинация на столе не найдена")
	ErrCannotExtend     = errors.New("карту нельзя подложить к этой комбинации")
	ErrTonkNotAllowed   = errors.New("вес руки превышает порог объявления Тонка")
	ErrDiscardPileEmpty = errors.New("стопка сброса пуста")
)

// DefaultTonkThreshold — по умолчанию Тонк можно объявить, если
// сумма очков карт на руке не превышает 5. Это игровое правило
// (не связано с деньгами) — вес руки, при котором вообще
// разрешено объявить Тонк.
const DefaultTonkThreshold = 5

// Денежные параметры по умолчанию: фиксированная ставка (анте) в
// банк раунда с каждого игрока, стартовый капитал и множитель
// банка при победе через объявление Тонка.
const (
	DefaultStartingCapital = 100
	DefaultAnte            = 5
	DefaultTonkMultiplier  = 2
)

// RoundResult — итог завершившегося раунда.
type RoundResult struct {
	DeadRound     bool  // колода и сброс исчерпаны, банк возвращён игрокам
	TonkCall      bool  // раунд завершён объявлением Тонка
	DropCall      bool  // раунд завершён объявлением дропа
	DropCaught    bool  // дроп был неудачным (объявившего "поймали")
	Winner        int   // индекс победителя (не имеет смысла при DeadRound)
	Pot           int   // размер банка на момент завершения раунда
	BalanceDeltas []int // изменение капитала каждого игрока за этот раунд
}

// GameState — полное состояние партии в Тонк.
type GameState struct {
	Players       []*PlayerState
	Deck          *Deck
	DiscardPile   []Card // последний элемент — верхняя карта
	Table         []TableMeld
	CurrentPlayer int
	Phase         Phase
	TonkThreshold int
	Result        *RoundResult // заполняется, когда Phase == PhaseGameOver

	// Денежная часть игры: фиксированная ставка с каждого игрока
	// уходит в банк раунда; обычная победа забирает банк целиком,
	// победа через Тонк — банк, умноженный на TonkMultiplier
	// (недостающую часть доплачивают проигравшие сверх анте).
	AnteAmount     int
	TonkMultiplier int
	Pot            int

	// StartingCapital сохраняется отдельно от текущего капитала
	// игроков — это сумма, с которой садится за стол новый игрок,
	// в том числе бот, заменяющий обанкротившегося (см. пакетный
	// уровень, функцию checkBankruptcies в tui.go).
	StartingCapital int

	// preRoundBalance — капитал каждого игрока непосредственно
	// перед списанием анте в начале раунда. Используется только
	// для вычисления BalanceDeltas в RoundResult (чтобы показать
	// игроку итог всего раунда целиком, а не отдельно анте и
	// отдельно выигрыш).
	preRoundBalance []int

	// noProgressTurns считает подряд идущие завершённые ходы, за
	// которые никто не выложил и не подложил ни одной карты в
	// комбинацию на столе. Без такого предохранителя игроки без
	// стратегии объявления Тонка теоретически могут застрять,
	// бесконечно беря и сбрасывая одну и ту же лишнюю карту —
	// такой раунд принудительно завершается вничью.
	noProgressTurns int
}

// maxNoProgressTurns — порог принудительного завершения "мёртвого"
// раунда, если долгое время не происходит никакого прогресса по
// комбинациям на столе.
const maxNoProgressTurns = 400

// NewGame создаёт новую партию со стандартными денежными
// параметрами (см. DefaultStartingCapital, DefaultAnte,
// DefaultTonkMultiplier). humanIdx — индекс живого игрока (может
// быть -1, если играют только боты, например для симуляций).
func NewGame(names []string, humanIdx int) *GameState {
	return NewGameWithStakes(names, humanIdx, DefaultStartingCapital, DefaultAnte, DefaultTonkMultiplier)
}

// NewGameWithStakes — то же самое, что NewGame, но с явно заданными
// денежными параметрами: стартовый капитал каждого игрока, размер
// фиксированной ставки (анте) в банк раунда и множитель банка при
// победе через Тонк.
func NewGameWithStakes(names []string, humanIdx, startingCapital, ante, tonkMultiplier int) *GameState {
	isHuman := make([]bool, len(names))
	if humanIdx >= 0 && humanIdx < len(names) {
		isHuman[humanIdx] = true
	}
	balances := make([]int, len(names))
	for i := range balances {
		balances[i] = startingCapital
	}
	return newRound(names, isHuman, balances, DefaultTonkThreshold, startingCapital, ante, tonkMultiplier)
}

// NextRound начинает новый раунд партии, сохраняя капитал игроков
// (и то, кто из них человек) из завершившегося раунда g. Ставка
// (анте) списывается заново из перенесённого капитала. Если перед
// вызовом NextRound в g.Players были подменены обанкротившиеся
// игроки (см. checkBankruptcies в tui.go), новый раунд стартует уже
// с обновлённым составом.
func (g *GameState) NextRound() *GameState {
	names := make([]string, len(g.Players))
	isHuman := make([]bool, len(g.Players))
	balances := make([]int, len(g.Players))
	for i, p := range g.Players {
		names[i] = p.Name
		isHuman[i] = p.IsHuman
		balances[i] = p.Balance
	}
	return newRound(names, isHuman, balances, g.TonkThreshold, g.StartingCapital, g.AnteAmount, g.TonkMultiplier)
}

// newRound — общая внутренняя логика раздачи новой партии/раунда:
// тасует колоду, раздаёт по 5 карт, вскрывает первую карту сброса
// и собирает анте в банк.
func newRound(names []string, isHuman []bool, balances []int, tonkThreshold, startingCapital, ante, tonkMultiplier int) *GameState {
	deck := NewDeck()
	deck.Shuffle()

	players := make([]*PlayerState, len(names))
	for i, name := range names {
		players[i] = &PlayerState{
			Name:    name,
			Hand:    deck.DrawN(5),
			IsHuman: isHuman[i],
			Balance: balances[i],
		}
	}

	firstDiscard, _ := deck.Draw()

	g := &GameState{
		Players:         players,
		Deck:            deck,
		DiscardPile:     []Card{firstDiscard},
		CurrentPlayer:   0,
		Phase:           PhaseDraw,
		TonkThreshold:   tonkThreshold,
		AnteAmount:      ante,
		TonkMultiplier:  tonkMultiplier,
		StartingCapital: startingCapital,
	}
	g.collectAnte()
	return g
}

// collectAnte списывает фиксированную ставку с каждого игрока в
// банк раунда. Капитал может уйти в минус, если у игрока не
// хватает средств на анте — это сознательное упрощение (реальный
// долг между игроками здесь не моделируется).
func (g *GameState) collectAnte() {
	g.preRoundBalance = make([]int, len(g.Players))
	for i, p := range g.Players {
		g.preRoundBalance[i] = p.Balance
		p.Balance -= g.AnteAmount
		g.Pot += g.AnteAmount
	}
}

func (g *GameState) current() *PlayerState {
	return g.Players[g.CurrentPlayer]
}

// PeekDiscard возвращает верхнюю карту стопки сброса.
func (g *GameState) PeekDiscard() (Card, bool) {
	if len(g.DiscardPile) == 0 {
		return Card{}, false
	}
	return g.DiscardPile[len(g.DiscardPile)-1], true
}

// reshuffleDiscardIntoDeck перекладывает стопку сброса (кроме
// верхней карты) обратно в колоду и тасует её — стандартный приём
// раммі-игр, когда колода добора заканчивается.
func (g *GameState) reshuffleDiscardIntoDeck() {
	if len(g.DiscardPile) <= 1 {
		return
	}
	top := g.DiscardPile[len(g.DiscardPile)-1]
	rest := g.DiscardPile[:len(g.DiscardPile)-1]
	g.Deck.cards = append(g.Deck.cards, rest...)
	g.Deck.Shuffle()
	g.DiscardPile = []Card{top}
}

// DrawFromDeck — взять карту из колоды добора. Если колода пуста,
// сначала пытается переложить сброс в колоду; если и это
// невозможно, раунд завершается вничью.
func (g *GameState) DrawFromDeck() (Card, error) {
	if g.Phase != PhaseDraw {
		return Card{}, ErrWrongPhase
	}
	if g.Deck.Len() == 0 {
		g.reshuffleDiscardIntoDeck()
	}
	card, ok := g.Deck.Draw()
	if !ok {
		g.endDeadRound()
		return Card{}, ErrDeckExhausted
	}
	g.current().Hand = append(g.current().Hand, card)
	g.Phase = PhaseAction
	return card, nil
}

// DrawFromDiscard — взять верхнюю карту из стопки сброса.
func (g *GameState) DrawFromDiscard() (Card, error) {
	if g.Phase != PhaseDraw {
		return Card{}, ErrWrongPhase
	}
	card, ok := g.PeekDiscard()
	if !ok {
		return Card{}, ErrDiscardPileEmpty
	}
	g.DiscardPile = g.DiscardPile[:len(g.DiscardPile)-1]
	g.current().Hand = append(g.current().Hand, card)
	g.Phase = PhaseAction
	return card, nil
}

// DeclareTonk — объявить Тонк. Разрешено только сразу после взятия
// карты (в фазе PhaseAction), до выкладывания комбинаций, и только
// если вес руки не превышает TonkThreshold.
func (g *GameState) DeclareTonk() (*RoundResult, error) {
	if g.Phase != PhaseAction {
		return nil, ErrWrongPhase
	}
	if !CanDeclareTonk(g.current().Hand, g.TonkThreshold) {
		return nil, ErrTonkNotAllowed
	}
	return g.endByTonk(g.CurrentPlayer), nil
}

// Drop — объявить дроп: рискованная заявка "у меня самая низкая
// сумма очков за столом", доступная в НАЧАЛЕ хода, до взятия карты
// (в отличие от Тонка, порог веса руки не требуется — можно
// дропнуть с любой рукой). Если объявивший действительно строго
// ниже всех остальных — он забирает банк как за обычную победу
// (без множителя). Если нет — он "попался": банк достаётся
// игроку(ам) с реально самой низкой суммой, а сам объявивший вдобавок
// платит им штраф в размере ещё одной ставки (анте) сверху.
func (g *GameState) Drop() (*RoundResult, error) {
	if g.Phase != PhaseDraw {
		return nil, ErrWrongPhase
	}
	return g.endByDrop(g.CurrentPlayer), nil
}

// LayNewMeld выкладывает новую комбинацию на стол из карт руки
// текущего игрока.
func (g *GameState) LayNewMeld(cards []Card) error {
	if g.Phase != PhaseAction {
		return ErrWrongPhase
	}
	for _, c := range cards {
		if !g.current().HasCard(c) {
			return ErrCardNotInHand
		}
	}
	var kind MeldKind
	switch {
	case IsSet(cards):
		kind = KindSet
	case IsSequence(cards):
		kind = KindSequence
	default:
		return ErrInvalidMeld
	}
	// копируем ДО удаления из руки: если вызывающий код передал сам
	// срез p.Hand (а не его копию), RemoveCards будет мутировать тот
	// же массив в памяти, и cards станет невалидным
	cardsCopy := append([]Card{}, cards...)
	g.current().RemoveCards(cardsCopy)
	g.Table = append(g.Table, TableMeld{
		Owner: g.CurrentPlayer,
		Kind:  kind,
		Cards: cardsCopy,
	})
	g.noProgressTurns = 0
	g.checkEmptyHandWin()
	return nil
}

// AddToMeld подкладывает одну карту руки текущего игрока к уже
// выложенной на столе комбинации (своей или чужой).
func (g *GameState) AddToMeld(meldIdx int, card Card) error {
	if g.Phase != PhaseAction {
		return ErrWrongPhase
	}
	if meldIdx < 0 || meldIdx >= len(g.Table) {
		return ErrMeldNotFound
	}
	if !g.current().HasCard(card) {
		return ErrCardNotInHand
	}
	meld := g.Table[meldIdx]
	trial := append(append([]Card{}, meld.Cards...), card)

	valid := false
	switch meld.Kind {
	case KindSet:
		valid = IsSet(trial)
	case KindSequence:
		valid = IsSequence(trial)
	}
	if !valid {
		return ErrCannotExtend
	}

	g.current().RemoveCard(card)
	g.Table[meldIdx].Cards = trial
	g.noProgressTurns = 0
	g.checkEmptyHandWin()
	return nil
}

// Discard сбрасывает карту и завершает ход текущего игрока.
func (g *GameState) Discard(card Card) error {
	if g.Phase != PhaseAction {
		return ErrWrongPhase
	}
	if g.Phase == PhaseGameOver {
		return ErrWrongPhase
	}
	if !g.current().HasCard(card) {
		return ErrCardNotInHand
	}
	g.current().RemoveCard(card)
	g.DiscardPile = append(g.DiscardPile, card)

	if len(g.current().Hand) == 0 {
		g.endByEmptyHand(g.CurrentPlayer)
		return nil
	}

	g.noProgressTurns++
	if g.noProgressTurns >= maxNoProgressTurns {
		g.endDeadRound()
		return nil
	}

	g.advanceTurn()
	return nil
}

// checkEmptyHandWin проверяет, не остался ли текущий игрок без
// карт сразу после выкладывания/подкладывания комбинации — в этом
// случае раунд заканчивается его победой без необходимости сброса.
func (g *GameState) checkEmptyHandWin() {
	if g.Phase == PhaseGameOver {
		return
	}
	if len(g.current().Hand) == 0 {
		g.endByEmptyHand(g.CurrentPlayer)
	}
}

func (g *GameState) advanceTurn() {
	g.CurrentPlayer = (g.CurrentPlayer + 1) % len(g.Players)
	g.Phase = PhaseDraw
}

// endByEmptyHand завершает раунд победой игрока winner, полностью
// избавившегося от карт. Победитель забирает весь банк раунда
// (сумму ставок всех игроков); остальные при этом уже заплатили
// свою часть банка в виде анте в начале раунда.
func (g *GameState) endByEmptyHand(winner int) {
	g.Players[winner].Balance += g.Pot
	pot := g.Pot
	g.Pot = 0

	g.Result = &RoundResult{
		Winner:        winner,
		Pot:           pot,
		BalanceDeltas: g.balanceDeltasFromPreRound(),
	}
	g.Phase = PhaseGameOver
}

// endByTonk завершает раунд объявлением Тонка игроком declarer.
// Объявивший забирает банк, умноженный на TonkMultiplier: помимо
// уже собранного банка, каждый из остальных игроков доплачивает
// поровну недостающую разницу (округление в меньшую сторону —
// остаток от деления забирает победитель, чтобы сумма выплат
// сходилась без потерь).
func (g *GameState) endByTonk(declarer int) *RoundResult {
	pot := g.Pot
	totalPayout := pot * g.TonkMultiplier
	extra := totalPayout - pot

	losers := make([]int, 0, len(g.Players)-1)
	for i := range g.Players {
		if i != declarer {
			losers = append(losers, i)
		}
	}
	actualExtra := 0
	if len(losers) > 0 {
		share := extra / len(losers)
		remainder := extra % len(losers)
		for idx, i := range losers {
			deduction := share
			if idx < remainder {
				// остаток от целочисленного деления распределяем по
				// первым проигравшим, чтобы сумма выплат сходилась
				// с точностью до копейки, а не терялась в округлении
				deduction++
			}
			g.Players[i].Balance -= deduction
			actualExtra += deduction
		}
	}
	g.Players[declarer].Balance += pot + actualExtra
	g.Pot = 0

	g.Result = &RoundResult{
		TonkCall:      true,
		Winner:        declarer,
		Pot:           pot,
		BalanceDeltas: g.balanceDeltasFromPreRound(),
	}
	g.Phase = PhaseGameOver
	return g.Result
}

// endByDrop разрешает объявление дропа игроком dropper: сравнивает
// вес его руки с весами рук всех остальных игроков.
//   - Если dropper строго ниже всех — он забирает банк как за
//     обычную победу (без множителя).
//   - Если нет — банк достаётся игроку(ам) с реально самой низкой
//     суммой (при нескольких таких игроках банк делится поровну), а
//     dropper вдобавок платит им штраф в размере ещё одной ставки
//     (анте) сверху, разделённой между ними так же поровну.
func (g *GameState) endByDrop(dropper int) *RoundResult {
	values := make([]int, len(g.Players))
	minValue := g.Players[dropper].HandValue()
	for i, p := range g.Players {
		values[i] = p.HandValue()
		if values[i] < minValue {
			minValue = values[i]
		}
	}

	var lowest []int
	for i, v := range values {
		if v == minValue {
			lowest = append(lowest, i)
		}
	}

	pot := g.Pot
	result := &RoundResult{DropCall: true, Pot: pot}

	if len(lowest) == 1 && lowest[0] == dropper {
		// успешный дроп: объявивший действительно строго ниже всех
		g.Players[dropper].Balance += pot
		result.Winner = dropper
	} else {
		// дроп не удался — "попался"; банк и штраф достаются
		// реальным обладателям самой низкой суммы (dropper среди них
		// быть не может: либо его сумма не минимальна, либо она
		// минимальна, но не единственна — в обоих случаях это не
		// строго меньше всех остальных)
		catchers := make([]int, 0, len(lowest))
		for _, i := range lowest {
			if i != dropper {
				catchers = append(catchers, i)
			}
		}
		penalty := g.AnteAmount
		if len(catchers) > 0 {
			potShare := pot / len(catchers)
			potRemainder := pot % len(catchers)
			penaltyShare := penalty / len(catchers)
			penaltyRemainder := penalty % len(catchers)
			totalPenaltyPaid := 0
			for idx, i := range catchers {
				share := potShare
				if idx < potRemainder {
					share++
				}
				pShare := penaltyShare
				if idx < penaltyRemainder {
					pShare++
				}
				g.Players[i].Balance += share + pShare
				totalPenaltyPaid += pShare
			}
			g.Players[dropper].Balance -= totalPenaltyPaid
		}
		result.Winner = catchers[0]
		result.DropCaught = true
	}

	g.Pot = 0
	result.BalanceDeltas = g.balanceDeltasFromPreRound()
	g.Result = result
	g.Phase = PhaseGameOver
	return result
}

// endDeadRound завершает раунд без победителя — колода и сброс
// исчерпаны. Банк возвращается игрокам поровну (каждому — его
// анте назад), чтобы никто не терял деньги в раунде без вины.
func (g *GameState) endDeadRound() {
	for _, p := range g.Players {
		p.Balance += g.AnteAmount
	}
	pot := g.Pot
	g.Pot = 0

	g.Result = &RoundResult{
		DeadRound:     true,
		Pot:           pot,
		BalanceDeltas: g.balanceDeltasFromPreRound(),
	}
	g.Phase = PhaseGameOver
}

// balanceDeltasFromPreRound считает итоговое изменение капитала
// каждого игрока за весь раунд целиком — от капитала до списания
// анте и до текущего момента после выплат. Это удобнее для
// отображения игроку, чем показывать анте и выигрыш раздельно.
func (g *GameState) balanceDeltasFromPreRound() []int {
	deltas := make([]int, len(g.Players))
	for i, p := range g.Players {
		deltas[i] = p.Balance - g.preRoundBalance[i]
	}
	return deltas
}

// String — компактное текстовое представление состояния стола,
// полезно для отладки и логирования симуляций.
func (g *GameState) String() string {
	top, _ := g.PeekDiscard()
	return fmt.Sprintf("ход: %s | фаза: %d | сброс сверху: %s | колода: %d карт",
		g.current().Name, g.Phase, top, g.Deck.Len())
}
