package main

import (
	"fmt"
	"strings"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// botMoveMsg сигнализирует о том, что пора сделать очередной ход
// бота — используется, чтобы ходы ботов были видны человеку с
// небольшой задержкой, а не проваливались мгновенно.
type botMoveMsg struct{}

// ColorChoice — выбор стороны в играх с двумя цветами: конкретный
// цвет (первый или второй в списке для этой игры) либо случайный
// выбор при старте партии. Общий тип для настроек Шашек, Шахмат,
// Го, Нард, Ура и Сенета — какая КОНКРЕТНО пара цветов ему
// соответствует ("белые/чёрные" и т.п.), решает уже сама игра.
type ColorChoice int

const (
	ColorChoiceA ColorChoice = iota
	ColorChoiceB
	ColorChoiceRandom
)

// colorChoiceLabel возвращает переведённую подпись для текущего
// выбора цвета; labelA/labelB — ключи переводов конкретных цветов
// этой игры (порядок зависит от игры — например, в Го традиционно
// первый ход за чёрными, поэтому там ColorChoiceA — "Чёрные").
func colorChoiceLabel(choice ColorChoice, labelAKey, labelBKey string) string {
	switch choice {
	case ColorChoiceA:
		return T(labelAKey)
	case ColorChoiceB:
		return T(labelBKey)
	default:
		return T("color.random")
	}
}

func (c *ColorChoice) cycle(delta int) {
	v := int(*c) + delta
	if v < 0 {
		v = 0
	}
	if v > int(ColorChoiceRandom) {
		v = int(ColorChoiceRandom)
	}
	*c = ColorChoice(v)
}

const botMoveDelay = 500 * time.Millisecond

// wrapText переносит длинный текст по ширине терминала — с разумным
// диапазоном: не уже 40 колонок (совсем узкие терминалы) и не шире
// 100 (сплошной текст на весь широкий терминал читать неудобно).
// Общая для всех игр — используется там, где в интерфейсе
// встречаются длинные абзацы (тексты квестов, сюжетные вставки и
// т.п.), которые иначе просто уезжали бы за правый край экрана без
// переноса строк.
func wrapText(text string, termWidth int) string {
	width := termWidth - 4
	if width < 40 {
		width = 60 // разумный запасной вариант, пока реальный размер ещё не пришёл через WindowSizeMsg
	}
	if width > 100 {
		width = 100
	}
	return lipgloss.NewStyle().Width(width).Render(text)
}

var (
	titleStyle     = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212"))
	redCardStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("203"))
	blackCardStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("255"))
	cursorStyle    = lipgloss.NewStyle().Reverse(true).Bold(true)
	dimStyle       = lipgloss.NewStyle().Faint(true)
	infoStyle      = lipgloss.NewStyle().Foreground(lipgloss.Color("87"))
	errorStyle     = lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true)
	turnStyle      = lipgloss.NewStyle().Foreground(lipgloss.Color("228")).Bold(true)
	winStyle       = lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Bold(true)
)

// Model — состояние TUI поверх GameState.
type Model struct {
	game     *GameState
	humanIdx int
	bots     map[int]BotStrategy

	cursor  int
	staged  map[Card]bool
	message string
	isError bool

	quitting   bool
	backToMenu bool
}

// NewModel создаёт новую партию со стандартными денежными
// параметрами: человек — игрок 0, дальше три бота со случайными
// именами и случайной (не подсказывающей ничего) комбинацией
// уровней сложности.
func NewModel() Model {
	return NewModelWithStakes(DefaultStartingCapital, DefaultAnte, DefaultTonkMultiplier)
}

// NewModelWithStakes создаёт новую партию с явно заданными
// денежными параметрами (стартовый капитал, размер ставки, множитель
// банка при победе через Тонк) — используется экраном настроек
// перед началом игры.
func NewModelWithStakes(startingCapital, ante, tonkMultiplier int) Model {
	botNames, botStrategies := newBotRoster(3)
	names := append([]string{T("common.you")}, botNames...)

	game := NewGameWithStakes(names, 0, startingCapital, ante, tonkMultiplier)
	m := Model{
		game:     game,
		humanIdx: 0,
		bots: map[int]BotStrategy{
			1: botStrategies[0],
			2: botStrategies[1],
			3: botStrategies[2],
		},
		staged: make(map[Card]bool),
	}
	return m
}

func (m Model) Init() tea.Cmd {
	return m.maybeScheduleBot()
}

// maybeScheduleBot возвращает команду, запускающую ход бота, если
// сейчас не очередь человека и раунд не завершён.
func (m Model) maybeScheduleBot() tea.Cmd {
	if m.game.Phase == PhaseGameOver {
		return nil
	}
	if m.game.CurrentPlayer == m.humanIdx {
		return nil
	}
	return tea.Tick(botMoveDelay, func(time.Time) tea.Msg { return botMoveMsg{} })
}

func (m *Model) resetSelectionState() {
	m.staged = make(map[Card]bool)
	if len(m.game.current().Hand) == 0 {
		m.cursor = 0
	} else if m.cursor >= len(m.game.current().Hand) {
		m.cursor = len(m.game.current().Hand) - 1
	}
}

func (m *Model) setInfo(format string, args ...interface{}) {
	m.message = fmt.Sprintf(format, args...)
	m.isError = false
}

func (m *Model) setError(err error) {
	m.message = T(err.Error())
	m.isError = true
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		return m.handleKey(msg)
	case botMoveMsg:
		return m.handleBotMove()
	}
	return m, nil
}

func (m Model) handleBotMove() (tea.Model, tea.Cmd) {
	if m.game.Phase == PhaseGameOver {
		return m, nil
	}
	idx := m.game.CurrentPlayer
	bot, ok := m.bots[idx]
	if !ok {
		// на текущего игрока бот не назначен (это человек) —
		// ничего не делаем, ждём ввода с клавиатуры
		return m, nil
	}
	if err := bot.PlayTurn(m.game); err != nil {
		m.setError(fmt.Errorf(T("бот %s: %w"), m.game.Players[idx].Name, err))
		return m, nil
	}
	m.setInfo(T("tonk.msg.bot_moved"), m.game.Players[idx].Name)
	if m.game.Phase != PhaseGameOver && m.game.CurrentPlayer == m.humanIdx {
		m.resetSelectionState()
	}
	return m, m.maybeScheduleBot()
}

func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	key := msg.String()

	if key == "ctrl+c" {
		m.quitting = true
		return m, tea.Quit
	}

	if m.game.Phase == PhaseGameOver {
		switch key {
		case "n":
			m.startNewRound()
			return m, m.maybeScheduleBot()
		case "q":
			m.backToMenu = true
			return m, nil
		}
		return m, nil
	}

	if key == "q" {
		m.backToMenu = true
		return m, nil
	}

	if m.game.CurrentPlayer != m.humanIdx {
		// сейчас ходит бот — игнорируем ввод
		return m, nil
	}

	switch m.game.Phase {
	case PhaseDraw:
		return m.handleDrawKey(key)
	case PhaseAction:
		return m.handleActionKey(key)
	}
	return m, nil
}

func (m Model) handleDrawKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "d":
		if _, err := m.game.DrawFromDeck(); err != nil {
			m.setError(err)
			return m, nil
		}
		m.setInfo(T("tonk.msg.drew_deck"))
		m.resetSelectionState()
	case "p":
		card, err := m.game.DrawFromDiscard()
		if err != nil {
			m.setError(err)
			return m, nil
		}
		m.setInfo(T("tonk.msg.drew_discard"), card)
		m.resetSelectionState()
	case "k":
		res, err := m.game.Drop()
		if err != nil {
			m.setError(err)
			return m, nil
		}
		if res.DropCaught {
			m.setInfo(T("tonk.msg.drop_caught"), m.game.Players[res.Winner].Name)
		} else {
			m.setInfo(T("tonk.msg.drop_success"))
		}
	}
	return m, nil
}

func (m Model) handleActionKey(key string) (tea.Model, tea.Cmd) {
	hand := m.game.current().Hand

	switch key {
	case "left", "h":
		if m.cursor > 0 {
			m.cursor--
		}
	case "right", "l":
		if m.cursor < len(hand)-1 {
			m.cursor++
		}
	case " ":
		if len(hand) == 0 {
			return m, nil
		}
		c := hand[m.cursor]
		if m.staged[c] {
			delete(m.staged, c)
		} else {
			m.staged[c] = true
		}
	case "m":
		return m.handleLayMeld()
	case "t":
		res, err := m.game.DeclareTonk()
		if err != nil {
			m.setError(err)
			return m, nil
		}
		m.setInfo(T("tonk.msg.tonk_declared"))
		_ = res
		return m, m.maybeScheduleBot()
	case "enter":
		if len(hand) == 0 {
			return m, nil
		}
		card := hand[m.cursor]
		if err := m.game.Discard(card); err != nil {
			m.setError(err)
			return m, nil
		}
		m.setInfo(T("tonk.msg.discarded"), card)
		if m.game.Phase != PhaseGameOver {
			m.resetSelectionState()
		}
		return m, m.maybeScheduleBot()
	case "1", "2", "3", "4", "5", "6", "7", "8", "9":
		idx := int(key[0] - '1')
		return m.handleAddToMeld(idx)
	}
	return m, nil
}

func (m Model) handleLayMeld() (tea.Model, tea.Cmd) {
	if len(m.staged) < 3 {
		m.setError(fmt.Errorf(T("выберите (пробел) минимум 3 карты для комбинации")))
		return m, nil
	}
	cards := make([]Card, 0, len(m.staged))
	for c := range m.staged {
		cards = append(cards, c)
	}
	if err := m.game.LayNewMeld(cards); err != nil {
		m.setError(err)
		return m, nil
	}
	m.setInfo(T("tonk.msg.meld_laid"))
	if m.game.Phase != PhaseGameOver {
		m.resetSelectionState()
		return m, nil
	}
	return m, m.maybeScheduleBot()
}

func (m Model) handleAddToMeld(meldIdx int) (tea.Model, tea.Cmd) {
	hand := m.game.current().Hand
	if len(hand) == 0 {
		return m, nil
	}
	if meldIdx < 0 || meldIdx >= len(m.game.Table) {
		m.setError(fmt.Errorf(T("на столе нет комбинации №%d"), meldIdx+1))
		return m, nil
	}
	card := hand[m.cursor]
	if err := m.game.AddToMeld(meldIdx, card); err != nil {
		m.setError(err)
		return m, nil
	}
	m.setInfo(T("tonk.msg.card_added"), card, meldIdx+1)
	if m.game.Phase != PhaseGameOver {
		m.resetSelectionState()
		return m, nil
	}
	return m, m.maybeScheduleBot()
}

// startNewRound начинает новый раунд, перенося капитал игроков из
// завершившегося раунда и заново собирая анте в банк. Перед этим
// проверяет, не остался ли кто-то без денег (см. checkBankruptcies)
// — такие игроки заменяются новыми ботами случайного уровня со
// стартовым капиталом.
func (m *Model) startNewRound() {
	bankruptMsg := m.checkBankruptcies()
	m.game = m.game.NextRound()
	m.staged = make(map[Card]bool)
	m.cursor = 0
	if bankruptMsg != "" {
		m.setInfo("%s", bankruptMsg)
	} else {
		m.message = ""
		m.isError = false
	}
}

// checkBankruptcies проверяет капитал каждого игрока после
// завершившегося раунда: у кого баланс опустился до нуля или ниже,
// тот выбывает и заменяется новым ботом — со случайным именем
// (по возможности не повторяющим тех, кто уже сидит за столом) и
// случайным уровнем сложности, который нигде не показывается. Если
// выбывает сам человек, его место тоже занимает бот, и humanIdx
// становится -1 (партия продолжается уже полностью автоматически,
// человек может понаблюдать или выйти в меню). Возвращает текстовое
// сообщение об произошедших заменах (пустая строка, если
// банкротств не было).
func (m *Model) checkBankruptcies() string {
	var messages []string
	for i, p := range m.game.Players {
		if p.Balance > 0 {
			continue
		}

		used := make(map[string]bool, len(m.game.Players))
		for _, other := range m.game.Players {
			used[other.Name] = true
		}
		newName := pickBotName(used)
		bot, _ := randomBotStrategy()
		wasHuman := p.IsHuman
		oldName := p.Name

		p.Name = newName
		p.Balance = m.game.StartingCapital
		p.IsHuman = false
		m.bots[i] = bot

		if wasHuman {
			m.humanIdx = -1
			messages = append(messages, fmt.Sprintf(T("tonk.bankrupt.human"), newName))
		} else {
			messages = append(messages, fmt.Sprintf(T("tonk.bankrupt.bot"), oldName, newName, p.Balance))
		}
	}
	return strings.Join(messages, " ")
}

func isRed(s Suit) bool {
	return s == Hearts || s == Diamonds
}

// cardColorStyle возвращает базовый (только цвет) стиль карты, не
// применяя его — используется, когда нужно доклеить к цвету ещё
// какие-то атрибуты (подчёркивание и т.п.) одним стилем, вместо
// вложенных вызовов Render (что ломается в некоторых терминалах).
func cardColorStyle(c Card) lipgloss.Style {
	if isRed(c.Suit) {
		return redCardStyle
	}
	return blackCardStyle
}

func renderCard(c Card) string {
	return cardColorStyle(c).Render(c.String())
}

// CardHighlight — состояние подсветки одной карты в ряду.
type CardHighlight int

const (
	CardNormal CardHighlight = iota
	CardCursor               // текущая позиция курсора
	CardStaged               // отложена под мелд (Тонк) — подчёркнута, цвет масти сохраняется
)

// cardBoxContentWidth — внутренняя ширина рамочной карты (вариант
// "B": ранг в левом верхнем углу, масть в правом нижнем). Подобрана
// как компромисс между похожестью на настоящую карту и тем, чтобы
// рука из нескольких карт не выходила за пределы узкого мобильного
// терминала.
const cardBoxContentWidth = 3

// cardsPerBoxRow — после скольких карт подряд рамочный ряд руки
// переносится на новую строку (см. renderCardBoxHand). Число
// фиксировано и не зависит от реальной ширины терминала —
// сознательное упрощение: 6 карт в ряд при content=3 укладываются в
// разумные ~35 символов на большинстве терминалов, а курсор при
// переносе остаётся линейным индексом — перенос влияет только на
// раскладку, не на навигацию.
const cardsPerBoxRow = 6

// renderCardBox рисует одну карту рамкой в 4 строки: верхняя рамка,
// ранг (выровнен по левому краю), масть (выровнена по правому
// краю — по диагонали от ранга), нижняя рамка.
//
// Курсор подсвечивается цветом РАМКИ (не текста) — иначе ранг и
// масть было бы не различить поверх инвертированных цветов, как
// раньше в однострочном варианте. "Отложена под мелд" — наоборот,
// подчёркнутый и жирный текст при обычной рамке, без инверсии цвета
// масти (важно для Тонка: там нужно видеть, какая именно карта
// отложена, не теряя красный/чёрный цвет).
func renderCardBox(c Card, highlight CardHighlight) []string {
	rank := c.Rank.String()
	for len([]rune(rank)) < cardBoxContentWidth {
		rank += " "
	}
	suit := c.Suit.String()
	for len([]rune(suit)) < cardBoxContentWidth {
		suit = " " + suit
	}

	var rankText, suitText string
	switch highlight {
	case CardStaged:
		// один стиль на весь текст, а не Render() поверх уже
		// отрендеренной строки — вложенные ANSI-последовательности
		// ломаются в некоторых терминалах (см. tui.go, renderHand)
		st := cardColorStyle(c).Underline(true).Bold(true)
		rankText, suitText = st.Render(rank), st.Render(suit)
	default:
		style := cardColorStyle(c)
		rankText, suitText = style.Render(rank), style.Render(suit)
	}

	top := "┌" + strings.Repeat("─", cardBoxContentWidth) + "┐"
	bottom := "└" + strings.Repeat("─", cardBoxContentWidth) + "┘"
	if highlight == CardCursor {
		top = turnStyle.Render(top)
		bottom = turnStyle.Render(bottom)
	}
	return []string{
		top,
		"│" + rankText + "│",
		"│" + suitText + "│",
		bottom,
	}
}

// cursorHighlights строит срез подсветки для игр, которым не нужно
// ничего, кроме курсора: все карты обычные, кроме позиции cursor.
func cursorHighlights(n, cursor int) []CardHighlight {
	hl := make([]CardHighlight, n)
	for i := range hl {
		if i == cursor {
			hl[i] = CardCursor
		}
	}
	return hl
}

// renderFaceDownBox рисует рамку карты рубашкой вниз (масть и ранг
// неизвестны) — той же ширины, что и обычная renderCardBox, чтобы
// ряды из известных и скрытых карт выравнивались друг с другом.
func renderFaceDownBox() []string {
	top := "┌" + strings.Repeat("─", cardBoxContentWidth) + "┐"
	bottom := "└" + strings.Repeat("─", cardBoxContentWidth) + "┘"
	mid := dimStyle.Render(" ? ")
	return []string{top, "│" + mid + "│", "│" + mid + "│", bottom}
}

// renderCardBoxRow рисует один ряд карт рамками (без переноса — см.
// renderCardBoxHand для руки произвольной длины), склеенных по
// горизонтали через один пробел.
func renderCardBoxRow(cards []Card, highlights []CardHighlight) string {
	if len(cards) == 0 {
		return ""
	}
	boxes := make([][]string, len(cards))
	for i, c := range cards {
		h := CardNormal
		if i < len(highlights) {
			h = highlights[i]
		}
		boxes[i] = renderCardBox(c, h)
	}
	lines := make([]string, 4)
	for row := 0; row < 4; row++ {
		parts := make([]string, len(cards))
		for i := range cards {
			parts[i] = boxes[i][row]
		}
		lines[row] = strings.Join(parts, " ")
	}
	return strings.Join(lines, "\n")
}

// renderCardBoxHand рисует руку карт произвольной длины, перенося
// каждые cardsPerBoxRow карт на новую строку. Курсор/отложенные
// карты остаются привязаны к линейному индексу в cards — перенос
// влияет только на раскладку, не на то, как считается позиция.
func renderCardBoxHand(cards []Card, highlights []CardHighlight) string {
	if len(cards) == 0 {
		return ""
	}
	var rows []string
	for start := 0; start < len(cards); start += cardsPerBoxRow {
		end := start + cardsPerBoxRow
		if end > len(cards) {
			end = len(cards)
		}
		rowHighlights := highlights
		if end <= len(highlights) {
			rowHighlights = highlights[start:end]
		} else if start < len(highlights) {
			rowHighlights = highlights[start:]
		} else {
			rowHighlights = nil
		}
		rows = append(rows, renderCardBoxRow(cards[start:end], rowHighlights))
	}
	return strings.Join(rows, "\n\n")
}

func (m Model) renderHand() string {
	hand := m.game.current().Hand
	if len(hand) == 0 {
		return dimStyle.Render(T("common.no_cards"))
	}
	highlights := make([]CardHighlight, len(hand))
	for i, c := range hand {
		switch {
		case i == m.cursor:
			highlights[i] = CardCursor
		case m.staged[c]:
			highlights[i] = CardStaged
		}
	}
	return renderCardBoxHand(hand, highlights)
}

func (m Model) renderTable() string {
	if len(m.game.Table) == 0 {
		return dimStyle.Render(T("tonk.table_empty"))
	}
	var b strings.Builder
	for i, meld := range m.game.Table {
		kind := T("tonk.set")
		if meld.Kind == KindSequence {
			kind = T("tonk.sequence")
		}
		cardsStr := make([]string, len(meld.Cards))
		for j, c := range meld.Cards {
			cardsStr[j] = renderCard(c)
		}
		fmt.Fprintf(&b, T("tonk.meld_line"), i+1, kind,
			m.game.Players[meld.Owner].Name, strings.Join(cardsStr, " "))
	}
	return strings.TrimRight(b.String(), "\n")
}

func (m Model) renderPlayers() string {
	var b strings.Builder
	for i, p := range m.game.Players {
		marker := "  "
		if i == m.game.CurrentPlayer {
			marker = "» "
		}
		name := p.Name
		line := fmt.Sprintf(T("tonk.player_line"), marker, name, len(p.Hand), p.Balance)
		if i == m.game.CurrentPlayer {
			line = turnStyle.Render(line)
		}
		fmt.Fprintln(&b, line)
	}
	return strings.TrimRight(b.String(), "\n")
}

func (m Model) View() string {
	if m.quitting {
		return T("common.goodbye")
	}

	var b strings.Builder
	fmt.Fprintln(&b, titleStyle.Render(T("tonk.title")))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, m.renderPlayers())
	fmt.Fprintln(&b)

	top, ok := m.game.PeekDiscard()
	topStr := "—"
	if ok {
		topStr = renderCard(top)
	}
	fmt.Fprintf(&b, T("tonk.status_line"),
		m.game.Deck.Len(), topStr, m.game.Pot, m.game.AnteAmount, m.game.TonkMultiplier)

	fmt.Fprintln(&b, T("tonk.melds_label"))
	fmt.Fprintln(&b, m.renderTable())
	fmt.Fprintln(&b)

	if m.game.Phase == PhaseGameOver {
		fmt.Fprintln(&b, m.renderRoundResult())
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, dimStyle.Render(T("tonk.hint.gameover")))
		return b.String()
	}

	if m.game.CurrentPlayer == m.humanIdx {
		fmt.Fprintln(&b, T("common.your_hand"))
		fmt.Fprintln(&b, m.renderHand())
		fmt.Fprintf(&b, T("tonk.hand_weight"), HandValue(m.game.current().Hand))

		switch m.game.Phase {
		case PhaseDraw:
			fmt.Fprintln(&b, Tf("tonk.hint.draw", topStr))
		case PhaseAction:
			tonkHint := ""
			if CanDeclareTonk(m.game.current().Hand, m.game.TonkThreshold) {
				tonkHint = winStyle.Render(T("tonk.hint.tonk_available"))
			}
			fmt.Fprintln(&b, T("tonk.hint.action1"))
			fmt.Fprintln(&b, T("tonk.hint.action2")+tonkHint)
		}
	} else {
		fmt.Fprintf(&b, T("common.thinking"), m.game.Players[m.game.CurrentPlayer].Name)
	}

	fmt.Fprintln(&b)
	if m.message != "" {
		if m.isError {
			fmt.Fprintln(&b, errorStyle.Render("! "+m.message))
		} else {
			fmt.Fprintln(&b, infoStyle.Render(m.message))
		}
	}

	return b.String()
}

func (m Model) renderRoundResult() string {
	res := m.game.Result
	var b strings.Builder
	switch {
	case res.DeadRound:
		fmt.Fprintln(&b, errorStyle.Render(Tf("tonk.result.dead", res.Pot)))
	case res.TonkCall:
		fmt.Fprintln(&b, winStyle.Render(Tf("tonk.result.tonk",
			m.game.Players[res.Winner].Name, m.game.TonkMultiplier, res.Pot*m.game.TonkMultiplier)))
	case res.DropCall && res.DropCaught:
		fmt.Fprintln(&b, errorStyle.Render(Tf("tonk.result.drop_caught", m.game.Players[res.Winner].Name)))
	case res.DropCall:
		fmt.Fprintln(&b, winStyle.Render(Tf("tonk.result.drop_success", m.game.Players[res.Winner].Name, res.Pot)))
	default:
		fmt.Fprintln(&b, winStyle.Render(Tf("tonk.result.win", m.game.Players[res.Winner].Name, res.Pot)))
	}
	for i, p := range m.game.Players {
		fmt.Fprintf(&b, T("tonk.result.delta_line"), p.Name, res.BalanceDeltas[i], p.Balance)
	}
	return strings.TrimRight(b.String(), "\n")
}
