package main

import "errors"

// ПРИМЕЧАНИЕ О ПРАВИЛАХ: у игры "101" существует много вариантов,
// расходящихся в деталях. Реализован следующий, наиболее часто
// встречающийся набор правил:
//   - Колода 36 карт (6-туз), как и в Дураке.
//   - Раздача: сдающий раздаёт по 5 карт каждому игроку, кроме
//     себя — себе он оставляет 4, а пятую (лишнюю) кладёт в центр
//     стола лицом вверх — это и есть стартовая карта сброса.
//   - Ход: нужно положить карту той же масти или того же ранга,
//     что и верхняя карта сброса. Если подходящей карты нет —
//     игрок обязан брать карты из колоды добора по одной, пока не
//     возьмёт подходящую (тогда сразу её играет) либо пока колода
//     не закончится (тогда ход просто переходит дальше).
//   - Специальные карты: туз пропускает ход следующего игрока;
//     шестёрка заставляет следующего взять 1 карту и пропустить
//     ход; семёрка — взять 2 карты и пропустить ход; пиковый
//     король — взять 4 карты и пропустить ход. Остальные короли и
//     карты обычные.
//   - Игрок, избавившийся от всех карт, выигрывает раунд. Если его
//     последней сыгранной картой была дама — он получает бонус
//     -20 очков за раунд (-40, если это была червовая дама) вместо
//     обычных 0; остальные складывают очковую стоимость карт,
//     оставшихся на руках, и добавляют к своему общему счёту.
//   - Набравший 101 очко и более выбывает из партии, если только
//     это не ровно 101 — тогда счёт обнуляется (традиционное
//     правило "отыграться"). Партия продолжается, пока не останется
//     один игрок — он и побеждает.

// OneOhOnePlayer — состояние одного игрока в партии 101.
type OneOhOnePlayer struct {
	Name    string
	Hand    []Card
	IsHuman bool
	Score   int // суммарный счёт по итогам сыгранных раундов
	Out     bool
}

// OneOhOnePhase — фаза текущего раунда.
type OneOhOnePhase int

const (
	// OneOhOnePhasePlay — обычный ход текущего игрока: сыграть карту
	// или взять из колоды добора.
	OneOhOnePhasePlay OneOhOnePhase = iota
	// OneOhOnePhaseChooseSuit — только что положили даму: тот же
	// игрок обязан назвать масть для следующего хода, прежде чем ход
	// перейдёт дальше (см. ChooseSuit).
	OneOhOnePhaseChooseSuit
	// OneOhOnePhaseRoundOver — раунд завершён.
	OneOhOnePhaseRoundOver
)

var (
	ErrOOOWrongPhase    = errors.New("недопустимое действие для текущей фазы")
	ErrOOOCardNotInHand = errors.New("карты нет на руке игрока")
	ErrOOOInvalidPlay   = errors.New("эта карта не подходит по масти или рангу")
	ErrOOOMustPlay      = errors.New("нужно сыграть карту, которая подходит — брать из колоды нельзя")
	ErrOOOInvalidSuit   = errors.New("недопустимая масть")
)

// OneOhOneRoundResult — итог одного завершившегося раунда.
type OneOhOneRoundResult struct {
	Winner      int
	QueenBonus  bool  // победитель закончил дамой (бонус учтён в ScoreDeltas)
	ScoreDeltas []int // изменение ОЧКОВ ЗА ЭТОТ РАУНД (не общего счёта)
	Eliminated  []int // индексы игроков, выбывших в этом раунде (счёт >101)
	Reset       []int // индексы игроков, у которых счёт обнулился (ровно 101)
	MatchOver   bool
	MatchWinner int  // имеет смысл только при MatchOver
	NoWinner    bool // вырожденный случай: все выбыли одновременно
	Stalemate   bool // раунд принудительно завершён защитой от зацикливания
}

// OneOhOneGameState — полное состояние партии в 101.
type OneOhOneGameState struct {
	Players []*OneOhOnePlayer

	Stock   []Card // колода добора; Stock[0] — верхняя (берётся первой)
	Discard []Card // сброс; последний элемент — верхняя (видимая) карта

	CurrentPlayerIdx int
	DealerIdx        int
	Phase            OneOhOnePhase
	Result           *OneOhOneRoundResult

	// DeclaredSuit — если не nil, названная масть (после дамы),
	// действующая вместо масти верхней карты сброса ровно для
	// следующего хода (см. effectiveSuit, ChooseSuit).
	DeclaredSuit *Suit

	// PendingEightSuit — если не nil, на столе лежит непокрытая
	// восьмёрка этой масти: следующий ход обязан быть либо той же
	// масти, либо другой восьмёркой (которая продолжит цепочку —
	// см. canPlay, applyEffectAndAdvance).
	PendingEightSuit *Suit

	turnsSinceStart int
}

// maxOneOhOneTurns — защита от зацикливания: если раунд длится
// неправдоподобно долго, принудительно завершаем его вничью (по
// текущему весу рук), чтобы партия гарантированно не зависла.
// Увеличено с 5000 после добавления обязательного покрытия
// восьмёрки — цепочки вынужденного добора честно могут занимать
// заметно больше ходов, чем раньше (замер показал до ~10000 на
// редкий раунд).
const maxOneOhOneTurns = 20000

// oneOhOneCardValue — очковая стоимость карты для подсчёта штрафа
// за карты, оставшиеся на руках.
func oneOhOneCardValue(r Rank) int {
	switch r {
	case Ace:
		return 11
	case Ten:
		return 10
	case Nine:
		return 0
	case Eight:
		return 8
	case Seven:
		return 7
	case Six:
		return 6
	case King:
		return 4
	case Queen:
		return 3
	case Jack:
		return 2
	}
	return 0
}

// NewOneOhOneGame создаёт новую партию: игрок 0 — первый сдающий.
func NewOneOhOneGame(names []string, humanIdx int) *OneOhOneGameState {
	players := make([]*OneOhOnePlayer, len(names))
	for i, name := range names {
		players[i] = &OneOhOnePlayer{Name: name, IsHuman: i == humanIdx}
	}
	g := &OneOhOneGameState{Players: players, DealerIdx: 0}
	g.dealNewRound()
	return g
}

// dealNewRound тасует колоду и раздаёт новый раунд: по 5 карт всем
// АКТИВНЫМ (не выбывшим) игрокам, кроме сдающего (ему 4, а "лишняя"
// пятая уходит в сброс как стартовая карта). Первым ходит игрок
// слева от сдающего.
func (g *OneOhOneGameState) dealNewRound() {
	deck := NewDurakDeck()
	deck.Shuffle()
	stock := deck.cards

	for _, p := range g.Players {
		p.Hand = nil
	}

	active := make([]int, 0, len(g.Players))
	for i, p := range g.Players {
		if !p.Out {
			active = append(active, i)
		}
	}

	for _, idx := range active {
		count := 5
		if idx == g.DealerIdx {
			count = 4
		}
		for c := 0; c < count; c++ {
			card := stock[0]
			stock = stock[1:]
			g.Players[idx].Hand = append(g.Players[idx].Hand, card)
		}
	}

	// "лишняя" пятая карта сдающего уходит в сброс как стартовая
	openingCard := stock[0]
	stock = stock[1:]

	g.Stock = stock
	g.Discard = []Card{openingCard}
	g.CurrentPlayerIdx = g.nextActive(g.DealerIdx)
	g.Phase = OneOhOnePhasePlay
	g.Result = nil
	g.turnsSinceStart = 0
	g.DeclaredSuit = nil
	if openingCard.Rank == Eight {
		suit := openingCard.Suit
		g.PendingEightSuit = &suit
	} else {
		g.PendingEightSuit = nil
	}
}

func (g *OneOhOneGameState) nextActive(from int) int {
	n := len(g.Players)
	for i := 1; i <= n; i++ {
		idx := (from + i) % n
		if !g.Players[idx].Out {
			return idx
		}
	}
	return from
}

func (g *OneOhOneGameState) current() *OneOhOnePlayer { return g.Players[g.CurrentPlayerIdx] }

func (g *OneOhOneGameState) topDiscard() Card {
	return g.Discard[len(g.Discard)-1]
}

// effectiveSuit возвращает "текущую" масть для проверки хода:
// обычно масть верхней карты сброса, но если недавно была положена
// дама и названа масть — используется названная масть вместо масти
// самой дамы (действует ровно один следующий ход).
func (g *OneOhOneGameState) effectiveSuit() Suit {
	if g.DeclaredSuit != nil {
		return *g.DeclaredSuit
	}
	return g.topDiscard().Suit
}

// canPlay проверяет, подходит ли карта по масти или рангу под
// верхнюю карту сброса.
//
// Три особых случая (в порядке приоритета):
//  1. Если на столе висит непокрытая восьмёрка (PendingEightSuit !=
//     nil) — годится ТОЛЬКО карта той же масти или другая восьмёрка;
//     ничего иного не подходит, даже дама.
//  2. Иначе дама кладётся на что угодно, независимо от масти и ранга.
//  3. Иначе — обычное правило: масть (с учётом объявленной после
//     прошлой дамы, если есть) или ранг верхней карты.
func (g *OneOhOneGameState) canPlay(card Card) bool {
	if g.PendingEightSuit != nil {
		return card.Suit == *g.PendingEightSuit || card.Rank == Eight
	}
	if card.Rank == Queen {
		return true
	}
	top := g.topDiscard()
	return card.Suit == g.effectiveSuit() || card.Rank == top.Rank
}

// hasPlayableCard проверяет, есть ли у игрока хоть одна карта,
// подходящая под текущий сброс.
func (g *OneOhOneGameState) hasPlayableCard(p *OneOhOnePlayer) bool {
	for _, c := range p.Hand {
		if g.canPlay(c) {
			return true
		}
	}
	return false
}

// drawOne снимает верхнюю карту колоды добора, при необходимости
// сначала перетасовывая сброс (кроме верхней карты) в новую колоду.
func (g *OneOhOneGameState) drawOne() Card {
	if len(g.Stock) == 0 {
		g.reshuffleStockFromDiscard()
	}
	if len(g.Stock) == 0 {
		return Card{}
	}
	c := g.Stock[0]
	g.Stock = g.Stock[1:]
	return c
}

// stockExhausted проверяет, остались ли вообще карты для добора (в
// колоде или в сбросе под верхней картой).
func (g *OneOhOneGameState) stockExhausted() bool {
	return len(g.Stock) == 0 && len(g.Discard) <= 1
}

func (g *OneOhOneGameState) reshuffleStockFromDiscard() {
	if len(g.Discard) <= 1 {
		return
	}
	top := g.Discard[len(g.Discard)-1]
	rest := g.Discard[:len(g.Discard)-1]
	newStock := &Deck{cards: append([]Card{}, rest...)}
	newStock.Shuffle()
	g.Stock = newStock.cards
	g.Discard = []Card{top}
}

// PlayCard — текущий игрок играет карту из руки.
func (g *OneOhOneGameState) PlayCard(card Card) error {
	if g.Phase != OneOhOnePhasePlay {
		return ErrOOOWrongPhase
	}
	p := g.current()
	if !hasCard(p.Hand, card) {
		return ErrOOOCardNotInHand
	}
	if !g.canPlay(card) {
		return ErrOOOInvalidPlay
	}

	p.Hand = removeCard(p.Hand, card)
	g.Discard = append(g.Discard, card)

	if len(p.Hand) == 0 {
		g.resolveRoundEnd(g.CurrentPlayerIdx, card)
		return nil
	}

	g.applyEffectAndAdvance(card)
	return nil
}

// Draw — текущий игрок берёт карту из колоды добора (разрешено
// только если у него нет ни одной подходящей карты на руке). Если
// взятая карта подходит — она сразу же автоматически играется;
// иначе колода добора снова спрашивается на следующем вызове Draw
// тем же игроком (в TUI/боте это просто повторные нажатия/вызовы).
func (g *OneOhOneGameState) Draw() error {
	if g.Phase != OneOhOnePhasePlay {
		return ErrOOOWrongPhase
	}
	p := g.current()
	if g.hasPlayableCard(p) {
		return ErrOOOMustPlay
	}
	if g.stockExhausted() {
		// брать неоткуда — ход переходит дальше без штрафов
		g.advanceTurnPlain()
		return nil
	}
	card := g.drawOne()
	p.Hand = append(p.Hand, card)
	if g.canPlay(card) {
		p.Hand = removeCard(p.Hand, card)
		g.Discard = append(g.Discard, card)
		if len(p.Hand) == 0 {
			g.resolveRoundEnd(g.CurrentPlayerIdx, card)
			return nil
		}
		g.applyEffectAndAdvance(card)
	}
	return nil
}

// applyEffectAndAdvance обрабатывает эффект специальной карты (если
// есть) и передаёт ход дальше.
//
// Восьмёрка выставляет/продолжает висящее требование "покрыть эту
// масть или другой восьмёркой" (см. PendingEightSuit) — само по себе
// не пропускает ходов и не даёт штрафов. Любая другая карта снимает
// это требование.
//
// Дама не передаёт ход сразу: игрок, который её положил, должен
// сначала явно назвать масть через ChooseSuit — только тогда ход
// пойдёт дальше.
func (g *OneOhOneGameState) applyEffectAndAdvance(played Card) {
	if played.Rank == Eight {
		suit := played.Suit
		g.PendingEightSuit = &suit
	} else {
		g.PendingEightSuit = nil
	}

	if played.Rank == Queen {
		g.Phase = OneOhOnePhaseChooseSuit
		return
	}

	g.DeclaredSuit = nil // объявленная масть действовала только на этот один ход
	g.advanceAfterEffect(played)
}

// ChooseSuit — игрок, только что положивший даму, называет масть,
// которая будет считаться "текущей" для следующего хода (вместо
// масти самой дамы). После этого ход передаётся дальше как обычно.
func (g *OneOhOneGameState) ChooseSuit(suit Suit) error {
	if g.Phase != OneOhOnePhaseChooseSuit {
		return ErrOOOWrongPhase
	}
	if suit < Clubs || suit > Spades {
		return ErrOOOInvalidSuit
	}
	g.DeclaredSuit = &suit
	g.Phase = OneOhOnePhasePlay
	g.advanceAfterEffect(g.topDiscard()) // сама дама не даёт скип/штраф — просто передаём ход
	return nil
}

// advanceAfterEffect считает эффект "скип + штраф" сыгранной карты и
// передаёт ход дальше — вынесено отдельно от applyEffectAndAdvance,
// чтобы вызываться и сразу для обычных карт, и из ChooseSuit после
// того, как масть для дамы уже названа.
func (g *OneOhOneGameState) advanceAfterEffect(played Card) {
	g.turnsSinceStart++
	if g.turnsSinceStart >= maxOneOhOneTurns {
		g.resolveStalemate()
		return
	}

	skip, penalty := oneOhOneCardEffect(played)
	if !skip {
		g.CurrentPlayerIdx = g.nextActive(g.CurrentPlayerIdx)
		return
	}

	skippedIdx := g.nextActive(g.CurrentPlayerIdx)
	skipped := g.Players[skippedIdx]
	for i := 0; i < penalty; i++ {
		if g.stockExhausted() {
			break
		}
		skipped.Hand = append(skipped.Hand, g.drawOne())
	}
	g.CurrentPlayerIdx = g.nextActive(skippedIdx)
}

func (g *OneOhOneGameState) advanceTurnPlain() {
	g.turnsSinceStart++
	if g.turnsSinceStart >= maxOneOhOneTurns {
		g.resolveStalemate()
		return
	}
	g.CurrentPlayerIdx = g.nextActive(g.CurrentPlayerIdx)
}

// oneOhOneCardEffect возвращает, пропускает ли карта следующего
// игрока и сколько штрафных карт он должен взять.
func oneOhOneCardEffect(c Card) (skip bool, penalty int) {
	switch {
	case c.Rank == Ace:
		return true, 0
	case c.Rank == Six:
		return true, 1
	case c.Rank == Seven:
		return true, 2
	case c.Rank == King && c.Suit == Spades:
		return true, 4
	default:
		return false, 0
	}
}

func (g *OneOhOneGameState) resolveStalemate() {
	g.Result = &OneOhOneRoundResult{Stalemate: true, ScoreDeltas: make([]int, len(g.Players))}
	g.Phase = OneOhOnePhaseRoundOver
}

// resolveRoundEnd завершает раунд победой игрока winnerIdx: считает
// штрафные очки остальных, бонус за даму, обновляет общий счёт и
// проверяет выбывание/окончание партии.
func (g *OneOhOneGameState) resolveRoundEnd(winnerIdx int, lastCard Card) {
	deltas := make([]int, len(g.Players))
	queenBonus := false

	if lastCard.Rank == Queen {
		queenBonus = true
		if lastCard.Suit == Hearts {
			deltas[winnerIdx] = -40
		} else {
			deltas[winnerIdx] = -20
		}
	}

	for i, p := range g.Players {
		if i == winnerIdx || p.Out {
			continue
		}
		sum := 0
		for _, c := range p.Hand {
			sum += oneOhOneCardValue(c.Rank)
		}
		deltas[i] = sum
	}

	for i, p := range g.Players {
		p.Score += deltas[i]
	}

	var eliminated, reset []int
	for i, p := range g.Players {
		if p.Score == 101 {
			p.Score = 0
			reset = append(reset, i)
		} else if p.Score > 101 {
			p.Out = true
			eliminated = append(eliminated, i)
		}
	}

	result := &OneOhOneRoundResult{
		Winner:      winnerIdx,
		QueenBonus:  queenBonus,
		ScoreDeltas: deltas,
		Eliminated:  eliminated,
		Reset:       reset,
	}

	active := 0
	lastActive := -1
	for i, p := range g.Players {
		if !p.Out {
			active++
			lastActive = i
		}
	}
	switch active {
	case 0:
		result.MatchOver = true
		result.NoWinner = true
	case 1:
		result.MatchOver = true
		result.MatchWinner = lastActive
	}

	g.Result = result
	g.Phase = OneOhOnePhaseRoundOver
	if !result.MatchOver {
		g.DealerIdx = winnerIdx
	}
}

// NextRound начинает новый раунд партии (сдающий — победитель
// предыдущего раунда), сохраняя счёт и статус выбывания игроков.
func (g *OneOhOneGameState) NextRound() {
	if g.Result != nil && g.Result.MatchOver {
		return
	}
	g.dealNewRound()
}
