package main

import "math/rand"

// BotStrategy — стратегия одного полного хода бота: взять карту,
// при необходимости выложить/подложить комбинации, сбросить карту
// (либо объявить Тонк, если это входит в стратегию).
type BotStrategy interface {
	PlayTurn(g *GameState) error
}

// dropRiskThreshold — вес руки, ниже которого боты готовы рискнуть
// дропом. Строже порога Тонка (см. DefaultTonkThreshold), потому что
// у дропа, в отличие от Тонка, нет встроенной защиты от ошибки:
// объявить его может каждый в начале своего хода, и при ошибке
// придётся доплатить штраф. Бот при этом решает только по своей
// руке — он не подглядывает в карты соперников, как и человек.
const dropRiskThreshold = 3

// maybeBotDrop проверяет, стоит ли боту рискнуть дропом прямо перед
// взятием карты (единственный момент, когда дроп разрешён). Если да
// — объявляет его и возвращает true (ход на этом закончен).
func maybeBotDrop(g *GameState) (bool, error) {
	if g.Phase != PhaseDraw {
		return false, nil
	}
	if HandValue(g.current().Hand) > dropRiskThreshold {
		return false, nil
	}
	_, err := g.Drop()
	return true, err
}

// botLevel — запись в реестре доступных уровней сложности ботов:
// используется и для явного назначения бота на место, и для
// случайного выбора (например, при замене выбывшего игрока).
type botLevel struct {
	Suffix string // короткое имя уровня для отображения в UI, напр. "средний"
	New    func() BotStrategy
}

var botLevels = []botLevel{
	{Suffix: "простой", New: func() BotStrategy { return SimpleBot{} }},
	{Suffix: "средний", New: func() BotStrategy { return MediumBot{} }},
	{Suffix: "Тонк", New: func() BotStrategy { return TonkBot{} }},
}

// randomBotStrategy возвращает бота случайного уровня сложности из
// botLevels вместе с человекочитаемым названием уровня. Уровень
// используется только во внутренней логике/тестах — в интерфейсе
// игрока он не показывается (уровень бота держится в секрете).
func randomBotStrategy() (BotStrategy, string) {
	lvl := botLevels[rand.Intn(len(botLevels))]
	return lvl.New(), lvl.Suffix
}

// botNamePool — имена, которые получают боты за столом. Уровень
// сложности в имени никак не отражается — игрок не должен заранее
// знать, с кем имеет дело.
var botNamePool = []string{"Костоправ", "Гоблин", "Одноглазый", "Ворон", "Ильмо"}

// botNamePoolEN — те же самые персонажи, но по-английски: смысловые
// прозвища переведены (Костоправ -> Bonesetter и т.д.), а не просто
// транслитерированы, чтобы сохранить характер имён.
var botNamePoolEN = []string{"Bonesetter", "Goblin", "One-Eye", "Raven", "Ilmo"}

func activeBotNamePool() []string {
	if CurrentLang() == LangEN {
		return botNamePoolEN
	}
	return botNamePool
}

// pickBotName выбирает случайное имя из активного (для текущего
// языка) пула имён, по возможности не повторяющее уже занятые имена
// (переданные в used). Если все имена из пула уже заняты, допускает
// повтор — это лучше, чем оставить место вовсе без имени.
func pickBotName(used map[string]bool) string {
	pool := activeBotNamePool()
	free := make([]string, 0, len(pool))
	for _, name := range pool {
		if !used[name] {
			free = append(free, name)
		}
	}
	if len(free) == 0 {
		free = pool
	}
	return free[rand.Intn(len(free))]
}

// newBotRoster формирует n мест за столом: у каждого — случайное
// (без повторов, если хватает имён в пуле) имя из botNamePool и
// независимо выбранный случайный уровень сложности. Порядок уровней
// не фиксирован (простой/средний/сложный может стоять в любом
// месте, включая повторы) — это осознанное решение, чтобы нельзя
// было угадать силу бота по его месту за столом.
func newBotRoster(n int) (names []string, strategies []BotStrategy) {
	used := make(map[string]bool, n)
	names = make([]string, n)
	strategies = make([]BotStrategy, n)
	for i := 0; i < n; i++ {
		name := pickBotName(used)
		used[name] = true
		names[i] = name

		strat, _ := randomBotStrategy()
		strategies[i] = strat
	}
	return names, strategies
}

// --- Уровень 1: простой (жадный) бот -----------------------------
//
// Берёт из сброса, только если карта сразу закрывает готовую
// комбинацию, иначе берёт из колоды. Выкладывает всё, что может.
// Сбрасывает самую тяжёлую карту, не участвующую ни в одной
// потенциальной комбинации.
type SimpleBot struct{}

func (SimpleBot) PlayTurn(g *GameState) error {
	if dropped, err := maybeBotDrop(g); dropped {
		return err
	}
	if err := simpleDrawDecision(g); err != nil {
		return err
	}
	if g.Phase == PhaseGameOver {
		return nil
	}
	// по правилам Тонк может объявить любой игрок, не только
	// специализированный TonkBot — иначе бот способен бесконечно
	// держать на руках одну ненужную карту
	if CanDeclareTonk(g.current().Hand, g.TonkThreshold) {
		_, err := g.DeclareTonk()
		return err
	}

	layAllAvailableMelds(g)
	if g.Phase == PhaseGameOver {
		return nil
	}
	tryExtendTableMelds(g)
	if g.Phase == PhaseGameOver {
		return nil
	}

	discard := chooseDiscardSimple(g.current().Hand)
	return g.Discard(discard)
}

func simpleDrawDecision(g *GameState) error {
	if top, ok := g.PeekDiscard(); ok && wouldFormMeld(g.current().Hand, top) {
		_, err := g.DrawFromDiscard()
		return err
	}
	_, err := g.DrawFromDeck()
	return err
}

// chooseDiscardSimple выбирает карту с нулевым потенциалом
// (не участвующую ни в одной намечающейся комбинации) и
// максимальной очковой стоимостью среди таких. Если все карты
// хоть немного полезны — сбрасывает просто самую дорогую.
func chooseDiscardSimple(hand []Card) Card {
	var deadWeight []Card
	for _, c := range hand {
		if cardPotential(hand, c) == 0 {
			deadWeight = append(deadWeight, c)
		}
	}
	pool := deadWeight
	if len(pool) == 0 {
		pool = hand
	}
	best := pool[0]
	for _, c := range pool[1:] {
		if c.Value() > best.Value() {
			best = c
		}
	}
	return best
}

// --- Уровень 2: средний бот ---------------------------------------
//
// Оценивает руку целиком через cardPotential: держит карты,
// близкие к комбинации, берёт из сброса не только при немедленном
// закрытии сета/сиквенса, но и при заметном приросте потенциала.
// Сбрасывает "мёртвый груз" — карту с наименьшим потенциалом.
type MediumBot struct{}

func (MediumBot) PlayTurn(g *GameState) error {
	if dropped, err := maybeBotDrop(g); dropped {
		return err
	}
	if err := mediumDrawDecision(g); err != nil {
		return err
	}
	if g.Phase == PhaseGameOver {
		return nil
	}
	if CanDeclareTonk(g.current().Hand, g.TonkThreshold) {
		_, err := g.DeclareTonk()
		return err
	}

	layAllAvailableMelds(g)
	if g.Phase == PhaseGameOver {
		return nil
	}
	tryExtendTableMelds(g)
	if g.Phase == PhaseGameOver {
		return nil
	}

	discard := deadestCard(g.current().Hand)
	return g.Discard(discard)
}

func mediumDrawDecision(g *GameState) error {
	top, ok := g.PeekDiscard()
	if ok {
		if wouldFormMeld(g.current().Hand, top) {
			_, err := g.DrawFromDiscard()
			return err
		}
		// Берём из сброса и без немедленного меlda, если карта
		// заметно усиливает руку (потенциал 2+ означает, что она
		// почти достраивает сет или сиквенс).
		trial := append(append([]Card{}, g.current().Hand...), top)
		if cardPotential(trial, top) >= 2 {
			_, err := g.DrawFromDiscard()
			return err
		}
	}
	_, err := g.DrawFromDeck()
	return err
}

// --- Уровень 3: бот со стратегией Тонка ---------------------------
//
// Действует как средний бот, но после каждого взятия карты
// проверяет, можно ли объявить Тонк, и делает это немедленно, не
// дожидаясь выкладывания комбинаций. Добавляет небольшую долю
// случайности в выбор сбрасываемой карты, чтобы не быть полностью
// предсказуемым.
type TonkBot struct {
	Threshold int // порог объявления Тонка; 0 — использовать g.TonkThreshold
}

func (b TonkBot) PlayTurn(g *GameState) error {
	if dropped, err := maybeBotDrop(g); dropped {
		return err
	}
	if err := mediumDrawDecision(g); err != nil {
		return err
	}
	if g.Phase == PhaseGameOver {
		return nil
	}

	threshold := b.Threshold
	if threshold == 0 {
		threshold = g.TonkThreshold
	}
	if CanDeclareTonk(g.current().Hand, threshold) {
		_, err := g.DeclareTonk()
		return err
	}

	layAllAvailableMelds(g)
	if g.Phase == PhaseGameOver {
		return nil
	}
	tryExtendTableMelds(g)
	if g.Phase == PhaseGameOver {
		return nil
	}

	discard := chooseDiscardWithNoise(g.current().Hand)
	return g.Discard(discard)
}

// chooseDiscardWithNoise в большинстве случаев сбрасывает самую
// "мёртвую" карту, но с вероятностью ~15% сбрасывает вторую по
// бесполезности — чтобы поведение бота было чуть менее
// предсказуемым для человека, играющего против него не первый раз.
func chooseDiscardWithNoise(hand []Card) Card {
	if len(hand) == 1 {
		return hand[0]
	}

	type scored struct {
		card      Card
		potential int
	}
	ranked := make([]scored, len(hand))
	for i, c := range hand {
		ranked[i] = scored{card: c, potential: cardPotential(hand, c)}
	}
	// сортировка пузырьком — рука короткая (обычно до 10-12 карт),
	// оптимизация не нужна
	for i := 0; i < len(ranked); i++ {
		for j := i + 1; j < len(ranked); j++ {
			worse := ranked[j].potential < ranked[i].potential ||
				(ranked[j].potential == ranked[i].potential && ranked[j].card.Value() > ranked[i].card.Value())
			if worse {
				ranked[i], ranked[j] = ranked[j], ranked[i]
			}
		}
	}

	if len(ranked) >= 2 && rand.Intn(100) < 15 {
		return ranked[1].card
	}
	return ranked[0].card
}
