package main

import (
	"fmt"
	"strings"

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

// GameEntry — запись в реестре игр коллекции. Регистр сделан так,
// чтобы добавление новой игры сводилось к добавлению одной записи
// в gameRegistry, без изменений в логике меню.
type GameEntry struct {
	Name        func() string
	Description func() string
	Rules       func() string
	New         func() tea.Model

	// UsesStakes — включает экран настройки ставки перед запуском:
	// стартовый капитал, размер анте и множитель банка при особой
	// победе (в Тонке — объявление Тонка). Если false, игра
	// запускается сразу через New(), без экрана настроек.
	UsesStakes    bool
	NewWithStakes func(startingCapital, ante, bonusMultiplier int) tea.Model

	// UsesPlayerCount — включает экран выбора количества игроков
	// (2-6, включая человека) перед запуском.
	UsesPlayerCount    bool
	NewWithPlayerCount func(numPlayers int) tea.Model

	// UsesBlackjackSettings — включает совмещённый экран настройки
	// перед запуском: число игроков, стартовый капитал и размер
	// ставки за раунд. Отдельный флаг вместо переиспользования
	// UsesStakes/UsesPlayerCount, потому что Блэкджеку нужны все три
	// параметра сразу на одном экране.
	UsesBlackjackSettings    bool
	NewBlackjackWithSettings func(numPlayers, startingCapital, bet int) tea.Model

	// UsesCheckersSettings — включает экран выбора уровня сложности
	// бота (простой/сильный) и цвета, за который играет человек,
	// перед началом партии.
	UsesCheckersSettings      bool
	NewCheckersWithDifficulty func(depth int, colorChoice ColorChoice) tea.Model

	// UsesTicTacToeSettings — включает экран выбора уровня сложности
	// бота (лёгкий/средний/непобедимый) перед партией.
	UsesTicTacToeSettings      bool
	NewTicTacToeWithDifficulty func(difficulty TicTacToeDifficulty) tea.Model

	// UsesNardySettings — включает экран выбора варианта правил
	// (короткие/длинные) и уровня сложности бота перед партией.
	UsesNardySettings    bool
	NewNardyWithSettings func(variant NardyVariant, difficulty NardyDifficulty, colorChoice ColorChoice) tea.Model

	// UsesUrSettings — включает экран выбора уровня сложности бота
	// перед партией в Ур.
	UsesUrSettings      bool
	NewUrWithDifficulty func(difficulty UrDifficulty, colorChoice ColorChoice) tea.Model

	// UsesSenetSettings — включает экран выбора уровня сложности
	// бота перед партией в Сенет.
	UsesSenetSettings      bool
	NewSenetWithDifficulty func(difficulty SenetDifficulty, colorChoice ColorChoice) tea.Model

	// UsesChessSettings — то же самое, но для шахмат (свой уровень
	// глубины поиска, т.к. ветвление в шахматах намного шире), плюс
	// выбор цвета.
	UsesChessSettings      bool
	NewChessWithDifficulty func(depth int, colorChoice ColorChoice) tea.Model

	// UsesGoSettings — экран с двумя полями: размер доски (9/13/19)
	// и уровень сложности бота.
	UsesGoSettings    bool
	NewGoWithSettings func(size int, difficulty GoDifficulty, colorChoice ColorChoice) tea.Model

	// UsesMinesweeperSettings — экран выбора пресета размера поля
	// (Новичок/Любитель/Эксперт).
	UsesMinesweeperSettings  bool
	NewMinesweeperWithPreset func(width, height, mines int) tea.Model
}

// gameRegistry — список доступных игр коллекции.
var gameRegistry = []GameEntry{
	{
		Name:          func() string { return T("game.tonk.name") },
		Description:   func() string { return T("game.tonk.desc") },
		Rules:         tonkRules,
		New:           func() tea.Model { return NewModel() },
		UsesStakes:    true,
		NewWithStakes: func(capital, ante, mult int) tea.Model { return NewModelWithStakes(capital, ante, mult) },
	},
	{
		Name:               func() string { return T("game.durak.name") },
		Description:        func() string { return T("game.durak.desc") },
		Rules:              durakRules,
		New:                func() tea.Model { return NewDurakModel(4) },
		UsesPlayerCount:    true,
		NewWithPlayerCount: func(n int) tea.Model { return NewDurakModel(n) },
	},
	{
		Name:                  func() string { return T("game.bj.name") },
		Description:           func() string { return T("game.bj.desc") },
		Rules:                 blackjackRules,
		UsesBlackjackSettings: true,
		NewBlackjackWithSettings: func(numPlayers, capital, bet int) tea.Model {
			return NewBlackjackModel(numPlayers, capital, bet)
		},
	},
	{
		Name:               func() string { return T("game.101.name") },
		Description:        func() string { return T("game.101.desc") },
		Rules:              oneOhOneRules,
		New:                func() tea.Model { return NewOneOhOneModel(4) },
		UsesPlayerCount:    true,
		NewWithPlayerCount: func(n int) tea.Model { return NewOneOhOneModel(n) },
	},
	{
		Name:        func() string { return T("game.1000.name") },
		Description: func() string { return T("game.1000.desc") },
		Rules:       thousandRules,
		New:         func() tea.Model { return NewThousandModel() },
	},
	{
		Name:        func() string { return T("game.klondike.name") },
		Description: func() string { return T("game.klondike.desc") },
		Rules:       klondikeRules,
		New:         func() tea.Model { return NewKlondikeModel() },
	},
	{
		Name:                 func() string { return T("game.checkers.name") },
		Description:          func() string { return T("game.checkers.desc") },
		Rules:                checkersRules,
		UsesCheckersSettings: true,
		NewCheckersWithDifficulty: func(depth int, colorChoice ColorChoice) tea.Model {
			return NewCheckersModel(depth, colorChoice)
		},
	},
	{
		Name:              func() string { return T("game.chess.name") },
		Description:       func() string { return T("game.chess.desc") },
		Rules:             chessRules,
		UsesChessSettings: true,
		NewChessWithDifficulty: func(depth int, colorChoice ColorChoice) tea.Model {
			return NewChessModel(depth, colorChoice)
		},
	},
	{
		Name:        func() string { return T("game.corpsestarch.name") },
		Description: func() string { return T("game.corpsestarch.desc") },
		Rules:       corpseStarchRules,
		New:         func() tea.Model { return NewCorpseStarchModel() },
	},
	{
		Name:           func() string { return T("game.go.name") },
		Description:    func() string { return T("game.go.desc") },
		Rules:          goRules,
		UsesGoSettings: true,
		NewGoWithSettings: func(size int, difficulty GoDifficulty, colorChoice ColorChoice) tea.Model {
			return NewGoModel(size, difficulty, colorChoice)
		},
	},
	{
		Name:                    func() string { return T("game.minesweeper.name") },
		Description:             func() string { return T("game.minesweeper.desc") },
		Rules:                   minesweeperRules,
		UsesMinesweeperSettings: true,
		NewMinesweeperWithPreset: func(width, height, mines int) tea.Model {
			return NewMinesweeperModel(width, height, mines)
		},
	},
	{
		Name:        func() string { return T("game.wayfarer.name") },
		Description: func() string { return T("game.wayfarer.desc") },
		Rules:       wayfarerRules,
		New:         func() tea.Model { return NewWayfarerModel() },
	},
	{
		Name:                  func() string { return T("game.tictactoe.name") },
		Description:           func() string { return T("game.tictactoe.desc") },
		Rules:                 ticTacToeRules,
		UsesTicTacToeSettings: true,
		NewTicTacToeWithDifficulty: func(difficulty TicTacToeDifficulty) tea.Model {
			return NewTicTacToeModel(difficulty)
		},
	},
	{
		Name:              func() string { return T("game.nardy.name") },
		Description:       func() string { return T("game.nardy.desc") },
		Rules:             nardyRules,
		UsesNardySettings: true,
		NewNardyWithSettings: func(variant NardyVariant, difficulty NardyDifficulty, colorChoice ColorChoice) tea.Model {
			return NewNardyModel(variant, difficulty, colorChoice)
		},
	},
	{
		Name:           func() string { return T("game.ur.name") },
		Description:    func() string { return T("game.ur.desc") },
		Rules:          urRules,
		UsesUrSettings: true,
		NewUrWithDifficulty: func(difficulty UrDifficulty, colorChoice ColorChoice) tea.Model {
			return NewUrModel(difficulty, colorChoice)
		},
	},
	{
		Name:              func() string { return T("game.senet.name") },
		Description:       func() string { return T("game.senet.desc") },
		Rules:             senetRules,
		UsesSenetSettings: true,
		NewSenetWithDifficulty: func(difficulty SenetDifficulty, colorChoice ColorChoice) tea.Model {
			return NewSenetModel(difficulty, colorChoice)
		},
	},
	{
		Name:               func() string { return T("game.poker.name") },
		Description:        func() string { return T("game.poker.desc") },
		Rules:              pokerRules,
		New:                func() tea.Model { return NewPokerModel(4) },
		UsesPlayerCount:    true,
		NewWithPlayerCount: func(n int) tea.Model { return NewPokerModel(n) },
	},
}

type appState int

const (
	stateLanguage appState = iota
	stateMenu
	stateRulesMenu
	stateRules
	stateSettings
	statePlayerCount
	stateBlackjackSettings
	stateCheckersSettings
	stateTicTacToeSettings
	stateNardySettings
	stateUrSettings
	stateSenetSettings
	stateChessSettings
	stateGoSettings
	stateMinesweeperSettings
	statePlaying
)

// stakeSettings — экран настройки ставки перед игрой: значения
// перебираются по пресетам (а не вводятся текстом), чтобы обойтись
// без отдельного text-input компонента в TUI.
type stakeSettings struct {
	capitalOptions    []int
	anteOptions       []int
	multiplierOptions []int

	capitalIdx    int
	anteIdx       int
	multiplierIdx int

	fieldCursor int // 0 = капитал, 1 = анте, 2 = множитель
	gameIdx     int // на какую игру из gameRegistry применится
}

func newStakeSettings(gameIdx int) stakeSettings {
	return stakeSettings{
		capitalOptions:    []int{50, 100, 200, 500, 1000},
		anteOptions:       []int{1, 5, 10, 20, 50},
		multiplierOptions: []int{2, 3, 4},
		capitalIdx:        1, // 100
		anteIdx:           1, // 5
		multiplierIdx:     0, // 2
		gameIdx:           gameIdx,
	}
}

func (s stakeSettings) capital() int    { return s.capitalOptions[s.capitalIdx] }
func (s stakeSettings) ante() int       { return s.anteOptions[s.anteIdx] }
func (s stakeSettings) multiplier() int { return s.multiplierOptions[s.multiplierIdx] }

// cycle сдвигает значение текущего выбранного поля на delta шагов
// (±1), с ограничением по краям списка пресетов (без зацикливания).
func (s *stakeSettings) cycle(delta int) {
	clamp := func(idx, max int) int {
		idx += delta
		if idx < 0 {
			return 0
		}
		if idx > max {
			return max
		}
		return idx
	}
	switch s.fieldCursor {
	case 0:
		s.capitalIdx = clamp(s.capitalIdx, len(s.capitalOptions)-1)
	case 1:
		s.anteIdx = clamp(s.anteIdx, len(s.anteOptions)-1)
	case 2:
		s.multiplierIdx = clamp(s.multiplierIdx, len(s.multiplierOptions)-1)
	}
}

// playerCountSettings — экран выбора количества игроков (2-6,
// включая человека) перед игрой вроде Дурака.
type playerCountSettings struct {
	count   int
	gameIdx int
}

func newPlayerCountSettings(gameIdx int) playerCountSettings {
	return playerCountSettings{count: 4, gameIdx: gameIdx}
}

func (s *playerCountSettings) cycle(delta int) {
	s.count += delta
	if s.count < 2 {
		s.count = 2
	}
	if s.count > 6 {
		s.count = 6
	}
}

// blackjackSettings — совмещённый экран настройки перед Блэкджеком:
// число игроков (1-6, включая человека — можно играть и в одиночку
// против дилера), стартовый капитал и размер ставки за раунд.
// checkersSettings — экран выбора уровня сложности бота перед
// партией в шашки: два уровня, разница только в глубине поиска.
type checkersSettings struct {
	levelIdx    int
	colorChoice ColorChoice
	fieldCursor int // 0 = сложность, 1 = цвет
	gameIdx     int
}

var checkersLevels = []struct {
	LabelKey string
	Depth    int
	DescKey  string
}{
	{LabelKey: "checkerslvl.easy", Depth: CheckersDifficultyEasy, DescKey: "checkerslvl.easy_desc"},
	{LabelKey: "checkerslvl.hard", Depth: CheckersDifficultyHard, DescKey: "checkerslvl.hard_desc"},
}

func newCheckersSettings(gameIdx int) checkersSettings {
	return checkersSettings{levelIdx: 0, gameIdx: gameIdx}
}

func (s *checkersSettings) cycle(delta int) {
	switch s.fieldCursor {
	case 0:
		s.levelIdx += delta
		if s.levelIdx < 0 {
			s.levelIdx = 0
		}
		if s.levelIdx > len(checkersLevels)-1 {
			s.levelIdx = len(checkersLevels) - 1
		}
	case 1:
		s.colorChoice.cycle(delta)
	}
}

// ticTacToeSettings — экран выбора уровня сложности бота перед
// партией в крестики-нолики: три уровня (лёгкий/средний/непобедимый).
type ticTacToeSettings struct {
	levelIdx int
	gameIdx  int
}

var ticTacToeLevels = []struct {
	LabelKey   string
	Difficulty TicTacToeDifficulty
	DescKey    string
}{
	{LabelKey: "tictactoelvl.easy", Difficulty: TicTacToeDifficultyEasy, DescKey: "tictactoelvl.easy_desc"},
	{LabelKey: "tictactoelvl.medium", Difficulty: TicTacToeDifficultyMedium, DescKey: "tictactoelvl.medium_desc"},
	{LabelKey: "tictactoelvl.hard", Difficulty: TicTacToeDifficultyHard, DescKey: "tictactoelvl.hard_desc"},
}

func newTicTacToeSettings(gameIdx int) ticTacToeSettings {
	return ticTacToeSettings{levelIdx: 0, gameIdx: gameIdx}
}

func (s *ticTacToeSettings) cycle(delta int) {
	s.levelIdx += delta
	if s.levelIdx < 0 {
		s.levelIdx = 0
	}
	if s.levelIdx > len(ticTacToeLevels)-1 {
		s.levelIdx = len(ticTacToeLevels) - 1
	}
}

// chessSettings — тот же самый экран выбора сложности, но для
// шахмат (свой уровень глубины поиска, т.к. ветвление в шахматах
// намного шире, чем в шашках).
type chessSettings struct {
	levelIdx    int
	colorChoice ColorChoice
	fieldCursor int // 0 = сложность, 1 = цвет
	gameIdx     int
}

var chessLevels = []struct {
	LabelKey string
	Depth    int
	DescKey  string
}{
	{LabelKey: "checkerslvl.easy", Depth: ChessDifficultyEasy, DescKey: "checkerslvl.easy_desc"},
	{LabelKey: "checkerslvl.hard", Depth: ChessDifficultyHard, DescKey: "checkerslvl.hard_desc"},
}

func newChessSettings(gameIdx int) chessSettings {
	return chessSettings{levelIdx: 0, gameIdx: gameIdx}
}

func (s *chessSettings) cycle(delta int) {
	switch s.fieldCursor {
	case 0:
		s.levelIdx += delta
		if s.levelIdx < 0 {
			s.levelIdx = 0
		}
		if s.levelIdx > len(chessLevels)-1 {
			s.levelIdx = len(chessLevels) - 1
		}
	case 1:
		s.colorChoice.cycle(delta)
	}
}

// goSettings — экран настройки партии в Го: два поля (размер доски
// и уровень сложности бота).
type goSettings struct {
	sizeOptions   []int
	sizeIdx       int
	difficultyIdx int
	colorChoice   ColorChoice
	fieldCursor   int // 0 = размер доски, 1 = сложность, 2 = цвет
	gameIdx       int
}

var goDifficultyLevels = []struct {
	LabelKey string
	Level    GoDifficulty
}{
	{LabelKey: "gosettings.level.easy", Level: GoDifficultyEasy},
	{LabelKey: "gosettings.level.medium", Level: GoDifficultyMedium},
	{LabelKey: "gosettings.level.hard", Level: GoDifficultyHard},
}

func newGoSettings(gameIdx int) goSettings {
	return goSettings{
		sizeOptions:   []int{9, 13, 19},
		sizeIdx:       0,
		difficultyIdx: 0,
		gameIdx:       gameIdx,
	}
}

func (s goSettings) size() int                { return s.sizeOptions[s.sizeIdx] }
func (s goSettings) difficulty() GoDifficulty { return goDifficultyLevels[s.difficultyIdx].Level }

func (s *goSettings) cycle(delta int) {
	clamp := func(idx, max int) int {
		idx += delta
		if idx < 0 {
			return 0
		}
		if idx > max {
			return max
		}
		return idx
	}
	switch s.fieldCursor {
	case 0:
		s.sizeIdx = clamp(s.sizeIdx, len(s.sizeOptions)-1)
	case 1:
		s.difficultyIdx = clamp(s.difficultyIdx, len(goDifficultyLevels)-1)
	case 2:
		s.colorChoice.cycle(delta)
	}
}

// nardySettings — экран настройки партии в нарды: выбор варианта
// правил (короткие/длинные) и уровня сложности бота.
type nardySettings struct {
	variantIdx    int
	difficultyIdx int
	colorChoice   ColorChoice
	fieldCursor   int // 0 = вариант правил, 1 = сложность, 2 = цвет
	gameIdx       int
}

var nardyVariantOptions = []struct {
	LabelKey string
	Variant  NardyVariant
}{
	{LabelKey: "nardysettings.variant.short", Variant: NardyShort},
	{LabelKey: "nardysettings.variant.long", Variant: NardyLong},
}

var nardyDifficultyLevels = []struct {
	LabelKey   string
	DescKey    string
	Difficulty NardyDifficulty
}{
	{LabelKey: "nardylvl.easy", DescKey: "nardylvl.easy_desc", Difficulty: NardyDifficultyEasy},
	{LabelKey: "nardylvl.medium", DescKey: "nardylvl.medium_desc", Difficulty: NardyDifficultyMedium},
	{LabelKey: "nardylvl.hard", DescKey: "nardylvl.hard_desc", Difficulty: NardyDifficultyHard},
}

func newNardySettings(gameIdx int) nardySettings {
	return nardySettings{gameIdx: gameIdx}
}

func (s nardySettings) variant() NardyVariant { return nardyVariantOptions[s.variantIdx].Variant }
func (s nardySettings) difficulty() NardyDifficulty {
	return nardyDifficultyLevels[s.difficultyIdx].Difficulty
}

func (s *nardySettings) cycle(delta int) {
	clamp := func(idx, max int) int {
		idx += delta
		if idx < 0 {
			return 0
		}
		if idx > max {
			return max
		}
		return idx
	}
	switch s.fieldCursor {
	case 0:
		s.variantIdx = clamp(s.variantIdx, len(nardyVariantOptions)-1)
	case 1:
		s.difficultyIdx = clamp(s.difficultyIdx, len(nardyDifficultyLevels)-1)
	case 2:
		s.colorChoice.cycle(delta)
	}
}

// urSettings — экран выбора уровня сложности бота перед партией в Ур.
type urSettings struct {
	levelIdx    int
	colorChoice ColorChoice
	fieldCursor int // 0 = сложность, 1 = цвет
	gameIdx     int
}

var urDifficultyLevels = []struct {
	LabelKey   string
	DescKey    string
	Difficulty UrDifficulty
}{
	{LabelKey: "urlvl.easy", DescKey: "urlvl.easy_desc", Difficulty: UrDifficultyEasy},
	{LabelKey: "urlvl.medium", DescKey: "urlvl.medium_desc", Difficulty: UrDifficultyMedium},
	{LabelKey: "urlvl.hard", DescKey: "urlvl.hard_desc", Difficulty: UrDifficultyHard},
}

func newUrSettings(gameIdx int) urSettings {
	return urSettings{gameIdx: gameIdx}
}

func (s *urSettings) cycle(delta int) {
	switch s.fieldCursor {
	case 0:
		s.levelIdx += delta
		if s.levelIdx < 0 {
			s.levelIdx = 0
		}
		if s.levelIdx > len(urDifficultyLevels)-1 {
			s.levelIdx = len(urDifficultyLevels) - 1
		}
	case 1:
		s.colorChoice.cycle(delta)
	}
}

// senetSettings — экран выбора уровня сложности бота перед партией
// в Сенет.
type senetSettings struct {
	levelIdx    int
	colorChoice ColorChoice
	fieldCursor int // 0 = сложность, 1 = цвет
	gameIdx     int
}

var senetDifficultyLevels = []struct {
	LabelKey   string
	DescKey    string
	Difficulty SenetDifficulty
}{
	{LabelKey: "senetlvl.easy", DescKey: "senetlvl.easy_desc", Difficulty: SenetDifficultyEasy},
	{LabelKey: "senetlvl.medium", DescKey: "senetlvl.medium_desc", Difficulty: SenetDifficultyMedium},
	{LabelKey: "senetlvl.hard", DescKey: "senetlvl.hard_desc", Difficulty: SenetDifficultyHard},
}

func newSenetSettings(gameIdx int) senetSettings {
	return senetSettings{gameIdx: gameIdx}
}

func (s *senetSettings) cycle(delta int) {
	switch s.fieldCursor {
	case 0:
		s.levelIdx += delta
		if s.levelIdx < 0 {
			s.levelIdx = 0
		}
		if s.levelIdx > len(senetDifficultyLevels)-1 {
			s.levelIdx = len(senetDifficultyLevels) - 1
		}
	case 1:
		s.colorChoice.cycle(delta)
	}
}

// minesweeperSettings — экран выбора пресета размера поля Сапёра.
type minesweeperSettings struct {
	presetIdx int
	gameIdx   int
}

var minesweeperPresets = []struct {
	LabelKey             string
	Width, Height, Mines int
}{
	{"mssettings.beginner", 9, 9, 10},
	{"mssettings.intermediate", 16, 16, 40},
	{"mssettings.expert", 30, 16, 99},
}

func newMinesweeperSettings(gameIdx int) minesweeperSettings {
	return minesweeperSettings{presetIdx: 0, gameIdx: gameIdx}
}

func (s *minesweeperSettings) cycle(delta int) {
	s.presetIdx += delta
	if s.presetIdx < 0 {
		s.presetIdx = 0
	}
	if s.presetIdx > len(minesweeperPresets)-1 {
		s.presetIdx = len(minesweeperPresets) - 1
	}
}

type blackjackSettings struct {
	capitalOptions []int
	betOptions     []int

	playerCount int
	capitalIdx  int
	betIdx      int
	fieldCursor int // 0 = число игроков, 1 = капитал, 2 = ставка
	gameIdx     int
}

func newBlackjackSettings(gameIdx int) blackjackSettings {
	return blackjackSettings{
		capitalOptions: []int{50, 100, 200, 500, 1000},
		betOptions:     []int{5, 10, 25, 50, 100},
		playerCount:    1,
		capitalIdx:     1, // 100
		betIdx:         1, // 10
		gameIdx:        gameIdx,
	}
}

func (s blackjackSettings) capital() int { return s.capitalOptions[s.capitalIdx] }
func (s blackjackSettings) bet() int     { return s.betOptions[s.betIdx] }

func (s *blackjackSettings) cycle(delta int) {
	clamp := func(idx, max int) int {
		idx += delta
		if idx < 0 {
			return 0
		}
		if idx > max {
			return max
		}
		return idx
	}
	switch s.fieldCursor {
	case 0:
		s.playerCount += delta
		if s.playerCount < 1 {
			s.playerCount = 1
		}
		if s.playerCount > 6 {
			s.playerCount = 6
		}
	case 1:
		s.capitalIdx = clamp(s.capitalIdx, len(s.capitalOptions)-1)
	case 2:
		s.betIdx = clamp(s.betIdx, len(s.betOptions)-1)
	}
}

var (
	menuTitleStyle    = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true)
	menuSubtitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Italic(true)
	menuItemStyle     = lipgloss.NewStyle()
	menuCursorStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("228")).Bold(true)
	menuDescStyle     = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
	menuHintStyle     = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Faint(true)
)

// menuOption — один пункт меню: либо запуск игры из реестра, либо
// служебное действие (правила/выход).
type menuOption struct {
	kind    string // "game", "rules", "quit"
	gameIdx int    // индекс в gameRegistry, если kind == "game"
}

// label возвращает актуальный (для текущего языка) текст пункта меню.
func (opt menuOption) label() string {
	switch opt.kind {
	case "game":
		return gameRegistry[opt.gameIdx].Name()
	case "rules":
		return T("menu.rules")
	case "language":
		return T("menu.language")
	case "quit":
		return T("menu.quit")
	}
	return ""
}

// desc возвращает актуальное (для текущего языка) описание пункта меню.
func (opt menuOption) desc() string {
	switch opt.kind {
	case "game":
		return gameRegistry[opt.gameIdx].Description()
	case "rules":
		return T("menu.rules_desc")
	case "language":
		return T("menu.language_desc")
	case "quit":
		return T("menu.quit_desc")
	}
	return ""
}

// AppModel — корневая модель приложения: меню, экран правил или
// запущенная игра.
type AppModel struct {
	state           appState
	langCursor      int
	cursor          int
	menuScroll      int // индекс первого видимого пункта главного меню
	rulesCursor     int
	rulesMenuScroll int // индекс первого видимого пункта подменю правил
	rulesScroll     int // сколько строк текста правил прокручено вниз
	termWidth       int
	termHeight      int
	options         []menuOption
	rulesText       string
	settings        stakeSettings
	playerCount     playerCountSettings
	blackjack       blackjackSettings
	checkers        checkersSettings
	ticTacToe       ticTacToeSettings
	nardy           nardySettings
	ur              urSettings
	senet           senetSettings
	chess           chessSettings
	goSetup         goSettings
	minesweeper     minesweeperSettings
	activeGame      tea.Model
	quitting        bool
}

func NewAppModel() AppModel {
	options := make([]menuOption, 0, len(gameRegistry)+3)
	for i := range gameRegistry {
		options = append(options, menuOption{kind: "game", gameIdx: i})
	}
	options = append(options, menuOption{kind: "rules"})
	options = append(options, menuOption{kind: "language"})
	options = append(options, menuOption{kind: "quit"})

	return AppModel{
		state:      stateLanguage,
		langCursor: int(CurrentLang()),
		options:    options,
		termHeight: 24,
		termWidth:  80,
	}
}

func (m AppModel) Init() tea.Cmd {
	return nil
}

func (m AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	if sizeMsg, ok := msg.(tea.WindowSizeMsg); ok {
		m.termWidth = sizeMsg.Width
		m.termHeight = sizeMsg.Height
		// на случай, если терминал реально изменили размером прямо во
		// время сессии — курсор мог оказаться за пределами уже другого
		// видимого окна, пересчитываем прокрутку под новую высоту
		m.menuScroll = clampWindowScroll(m.cursor, m.menuScroll, m.menuVisibleItems(), len(m.options))
		m.rulesMenuScroll = clampWindowScroll(m.rulesCursor, m.rulesMenuScroll, m.rulesMenuVisibleItems(), len(gameRegistry))
		m.clampRulesScroll()
		// пропускаем дальше — активная игра тоже может интересоваться
		// размером окна в будущем; сейчас просто не теряем ход
		if m.state == statePlaying && m.activeGame != nil {
			updated, cmd := m.activeGame.Update(msg)
			m.activeGame = updated
			return m, cmd
		}
		return m, nil
	}

	switch m.state {
	case statePlaying:
		return m.updatePlaying(msg)
	case stateLanguage:
		return m.updateLanguage(msg)
	case stateRulesMenu:
		return m.updateRulesMenu(msg)
	case stateRules:
		return m.updateRules(msg)
	case stateSettings:
		return m.updateSettings(msg)
	case statePlayerCount:
		return m.updatePlayerCount(msg)
	case stateBlackjackSettings:
		return m.updateBlackjackSettings(msg)
	case stateCheckersSettings:
		return m.updateCheckersSettings(msg)
	case stateTicTacToeSettings:
		return m.updateTicTacToeSettings(msg)
	case stateNardySettings:
		return m.updateNardySettings(msg)
	case stateUrSettings:
		return m.updateUrSettings(msg)
	case stateSenetSettings:
		return m.updateSenetSettings(msg)
	case stateChessSettings:
		return m.updateChessSettings(msg)
	case stateGoSettings:
		return m.updateGoSettings(msg)
	case stateMinesweeperSettings:
		return m.updateMinesweeperSettings(msg)
	default:
		return m.updateMenu(msg)
	}
}

func (m AppModel) updateLanguage(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "up", "k":
		if m.langCursor > 0 {
			m.langCursor--
		}
	case "down", "j":
		if m.langCursor < 1 {
			m.langCursor++
		}
	case "enter":
		SetLanguage(Lang(m.langCursor))
		m.state = stateMenu
	}
	return m, nil
}

func (m AppModel) updateMenu(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c", "q":
		m.quitting = true
		return m, tea.Quit
	case "up", "k":
		if m.cursor > 0 {
			m.cursor--
		}
		m.menuScroll = clampWindowScroll(m.cursor, m.menuScroll, m.menuVisibleItems(), len(m.options))
	case "down", "j":
		if m.cursor < len(m.options)-1 {
			m.cursor++
		}
		m.menuScroll = clampWindowScroll(m.cursor, m.menuScroll, m.menuVisibleItems(), len(m.options))
	case "enter":
		selected := m.options[m.cursor]
		switch selected.kind {
		case "game":
			entry := gameRegistry[selected.gameIdx]
			switch {
			case entry.UsesStakes:
				m.settings = newStakeSettings(selected.gameIdx)
				m.state = stateSettings
				return m, nil
			case entry.UsesPlayerCount:
				m.playerCount = newPlayerCountSettings(selected.gameIdx)
				m.state = statePlayerCount
				return m, nil
			case entry.UsesBlackjackSettings:
				m.blackjack = newBlackjackSettings(selected.gameIdx)
				m.state = stateBlackjackSettings
				return m, nil
			case entry.UsesCheckersSettings:
				m.checkers = newCheckersSettings(selected.gameIdx)
				m.state = stateCheckersSettings
				return m, nil
			case entry.UsesTicTacToeSettings:
				m.ticTacToe = newTicTacToeSettings(selected.gameIdx)
				m.state = stateTicTacToeSettings
				return m, nil
			case entry.UsesNardySettings:
				m.nardy = newNardySettings(selected.gameIdx)
				m.state = stateNardySettings
				return m, nil
			case entry.UsesUrSettings:
				m.ur = newUrSettings(selected.gameIdx)
				m.state = stateUrSettings
				return m, nil
			case entry.UsesSenetSettings:
				m.senet = newSenetSettings(selected.gameIdx)
				m.state = stateSenetSettings
				return m, nil
			case entry.UsesChessSettings:
				m.chess = newChessSettings(selected.gameIdx)
				m.state = stateChessSettings
				return m, nil
			case entry.UsesGoSettings:
				m.goSetup = newGoSettings(selected.gameIdx)
				m.state = stateGoSettings
				return m, nil
			case entry.UsesMinesweeperSettings:
				m.minesweeper = newMinesweeperSettings(selected.gameIdx)
				m.state = stateMinesweeperSettings
				return m, nil
			}
			m.state = statePlaying
			m.activeGame = entry.New()
			return m, m.activeGame.Init()
		case "rules":
			m.rulesCursor = 0
			m.rulesMenuScroll = 0
			m.state = stateRulesMenu
		case "language":
			m.langCursor = int(CurrentLang())
			m.state = stateLanguage
		case "quit":
			m.quitting = true
			return m, tea.Quit
		}
	}
	return m, nil
}

func (m AppModel) updateRulesMenu(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
	case "up", "k":
		if m.rulesCursor > 0 {
			m.rulesCursor--
		}
		m.rulesMenuScroll = clampWindowScroll(m.rulesCursor, m.rulesMenuScroll, m.rulesMenuVisibleItems(), len(gameRegistry))
	case "down", "j":
		if m.rulesCursor < len(gameRegistry)-1 {
			m.rulesCursor++
		}
		m.rulesMenuScroll = clampWindowScroll(m.rulesCursor, m.rulesMenuScroll, m.rulesMenuVisibleItems(), len(gameRegistry))
	case "enter":
		m.rulesText = gameRegistry[m.rulesCursor].Rules()
		m.rulesScroll = 0
		m.state = stateRules
	}
	return m, nil
}

// clampWindowScroll подбирает минимальную корректировку scroll так,
// чтобы cursor гарантированно попадал в видимое окно
// [scroll, scroll+visibleCount) — используется и для списка игр в
// главном меню, и для подменю правил, чтобы курсор при навигации
// стрелками никогда не убегал за пределы экрана.
func clampWindowScroll(cursor, scroll, visibleCount, total int) int {
	if visibleCount <= 0 {
		return 0
	}
	if cursor < scroll {
		scroll = cursor
	}
	if cursor >= scroll+visibleCount {
		scroll = cursor - visibleCount + 1
	}
	if scroll < 0 {
		scroll = 0
	}
	maxScroll := total - visibleCount
	if maxScroll < 0 {
		maxScroll = 0
	}
	if scroll > maxScroll {
		scroll = maxScroll
	}
	return scroll
}

// asciiBannerLines — число строк в ASCII-баннере (для расчёта, сколько
// места остаётся под сам список пунктов меню).
func asciiBannerLines() int {
	return strings.Count(asciiBanner, "\n") + 1
}

// menuBannerCompactThreshold — если высота терминала меньше этого
// значения, полноразмерный ASCII-баннер (13 строк) заменяется на
// компактную однострочную надпись — иначе на невысоких терминалах
// под сам список игр совсем не остаётся места.
const menuBannerCompactThreshold = 30

const menuBannerCompact = "=== GO GAMES COLLECTION ==="

// menuUsesCompactBanner решает, показывать ли компактный заголовок
// вместо полного ASCII-баннера при текущей высоте терминала.
func (m AppModel) menuUsesCompactBanner() bool {
	return m.termHeight < menuBannerCompactThreshold
}

// menuBannerLineCount — сколько строк реально займёт заголовок меню
// (баннер или его компактная замена) при текущей высоте терминала.
func (m AppModel) menuBannerLineCount() int {
	if m.menuUsesCompactBanner() {
		return 1
	}
	return asciiBannerLines()
}

// menuVisibleItems — сколько пунктов главного меню помещается на
// экран одновременно. Каждый пункт занимает 2 строки (название +
// описание), плюс заголовок, подзаголовок, пустая строка и подсказка
// снизу — фиксированный "хвост" интерфейса.
func (m AppModel) menuVisibleItems() int {
	overhead := m.menuBannerLineCount() + 4 + 2 // подзаголовок + 2 пустые строки + подсказка + место под ▲/▼
	available := m.termHeight - overhead
	items := available / 2
	if items < 1 {
		items = 1
	}
	return items
}

// rulesMenuVisibleItems — сколько пунктов подменю правил помещается
// на экран одновременно (по одной строке на пункт, без описания).
func (m AppModel) rulesMenuVisibleItems() int {
	overhead := 4 + 2 // заголовок + пустая строка + пустая строка + подсказка + место под ▲/▼
	available := m.termHeight - overhead
	if available < 1 {
		available = 1
	}
	return available
}

// rulesVisibleLines — сколько строк текста правил помещается на
// экран одновременно (с учётом заголовка и подсказки снизу).
func (m AppModel) rulesVisibleLines() int {
	h := m.termHeight - 4
	if h < 3 {
		h = 3
	}
	return h
}

func (m AppModel) rulesTotalLines() int {
	return len(strings.Split(m.rulesText, "\n"))
}

func (m AppModel) rulesMaxScroll() int {
	max := m.rulesTotalLines() - m.rulesVisibleLines()
	if max < 0 {
		max = 0
	}
	return max
}

func (m *AppModel) clampRulesScroll() {
	if m.rulesScroll < 0 {
		m.rulesScroll = 0
	}
	if max := m.rulesMaxScroll(); m.rulesScroll > max {
		m.rulesScroll = max
	}
}

func (m AppModel) updateRules(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "q", "esc":
		m.state = stateRulesMenu
		return m, nil
	case "up", "k":
		m.rulesScroll--
	case "down", "j":
		m.rulesScroll++
	case "pgup", "b":
		m.rulesScroll -= m.rulesVisibleLines()
	case "pgdown", "f", " ":
		m.rulesScroll += m.rulesVisibleLines()
	case "home", "g":
		m.rulesScroll = 0
	case "end", "G":
		m.rulesScroll = m.rulesMaxScroll()
	}
	m.clampRulesScroll()
	return m, nil
}

func (m AppModel) updatePlayerCount(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "left", "h", "down", "j":
		m.playerCount.cycle(-1)
	case "right", "l", "up", "k":
		m.playerCount.cycle(1)
	case "enter":
		entry := gameRegistry[m.playerCount.gameIdx]
		m.activeGame = entry.NewWithPlayerCount(m.playerCount.count)
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateBlackjackSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k":
		if m.blackjack.fieldCursor > 0 {
			m.blackjack.fieldCursor--
		}
	case "down", "j":
		if m.blackjack.fieldCursor < 2 {
			m.blackjack.fieldCursor++
		}
	case "left", "h":
		m.blackjack.cycle(-1)
	case "right", "l":
		m.blackjack.cycle(1)
	case "enter":
		entry := gameRegistry[m.blackjack.gameIdx]
		m.activeGame = entry.NewBlackjackWithSettings(m.blackjack.playerCount, m.blackjack.capital(), m.blackjack.bet())
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateCheckersSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k":
		if m.checkers.fieldCursor > 0 {
			m.checkers.fieldCursor--
		}
	case "down", "j":
		if m.checkers.fieldCursor < 1 {
			m.checkers.fieldCursor++
		}
	case "left", "h":
		m.checkers.cycle(-1)
	case "right", "l":
		m.checkers.cycle(1)
	case "enter":
		entry := gameRegistry[m.checkers.gameIdx]
		depth := checkersLevels[m.checkers.levelIdx].Depth
		m.activeGame = entry.NewCheckersWithDifficulty(depth, m.checkers.colorChoice)
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateTicTacToeSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k", "left", "h":
		m.ticTacToe.cycle(-1)
	case "down", "j", "right", "l":
		m.ticTacToe.cycle(1)
	case "enter":
		entry := gameRegistry[m.ticTacToe.gameIdx]
		difficulty := ticTacToeLevels[m.ticTacToe.levelIdx].Difficulty
		m.activeGame = entry.NewTicTacToeWithDifficulty(difficulty)
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateNardySettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k":
		if m.nardy.fieldCursor > 0 {
			m.nardy.fieldCursor--
		}
	case "down", "j":
		if m.nardy.fieldCursor < 2 {
			m.nardy.fieldCursor++
		}
	case "left", "h":
		m.nardy.cycle(-1)
	case "right", "l":
		m.nardy.cycle(1)
	case "enter":
		entry := gameRegistry[m.nardy.gameIdx]
		m.activeGame = entry.NewNardyWithSettings(m.nardy.variant(), m.nardy.difficulty(), m.nardy.colorChoice)
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateUrSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k":
		if m.ur.fieldCursor > 0 {
			m.ur.fieldCursor--
		}
	case "down", "j":
		if m.ur.fieldCursor < 1 {
			m.ur.fieldCursor++
		}
	case "left", "h":
		m.ur.cycle(-1)
	case "right", "l":
		m.ur.cycle(1)
	case "enter":
		entry := gameRegistry[m.ur.gameIdx]
		difficulty := urDifficultyLevels[m.ur.levelIdx].Difficulty
		m.activeGame = entry.NewUrWithDifficulty(difficulty, m.ur.colorChoice)
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateSenetSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k":
		if m.senet.fieldCursor > 0 {
			m.senet.fieldCursor--
		}
	case "down", "j":
		if m.senet.fieldCursor < 1 {
			m.senet.fieldCursor++
		}
	case "left", "h":
		m.senet.cycle(-1)
	case "right", "l":
		m.senet.cycle(1)
	case "enter":
		entry := gameRegistry[m.senet.gameIdx]
		difficulty := senetDifficultyLevels[m.senet.levelIdx].Difficulty
		m.activeGame = entry.NewSenetWithDifficulty(difficulty, m.senet.colorChoice)
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateChessSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k":
		if m.chess.fieldCursor > 0 {
			m.chess.fieldCursor--
		}
	case "down", "j":
		if m.chess.fieldCursor < 1 {
			m.chess.fieldCursor++
		}
	case "left", "h":
		m.chess.cycle(-1)
	case "right", "l":
		m.chess.cycle(1)
	case "enter":
		entry := gameRegistry[m.chess.gameIdx]
		depth := chessLevels[m.chess.levelIdx].Depth
		m.activeGame = entry.NewChessWithDifficulty(depth, m.chess.colorChoice)
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateGoSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k":
		if m.goSetup.fieldCursor > 0 {
			m.goSetup.fieldCursor--
		}
	case "down", "j":
		if m.goSetup.fieldCursor < 2 {
			m.goSetup.fieldCursor++
		}
	case "left", "h":
		m.goSetup.cycle(-1)
	case "right", "l":
		m.goSetup.cycle(1)
	case "enter":
		entry := gameRegistry[m.goSetup.gameIdx]
		m.activeGame = entry.NewGoWithSettings(m.goSetup.size(), m.goSetup.difficulty(), m.goSetup.colorChoice)
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateMinesweeperSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k", "left", "h":
		m.minesweeper.cycle(-1)
	case "down", "j", "right", "l":
		m.minesweeper.cycle(1)
	case "enter":
		entry := gameRegistry[m.minesweeper.gameIdx]
		p := minesweeperPresets[m.minesweeper.presetIdx]
		m.activeGame = entry.NewMinesweeperWithPreset(p.Width, p.Height, p.Mines)
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	switch keyMsg.String() {
	case "ctrl+c":
		m.quitting = true
		return m, tea.Quit
	case "esc", "q":
		m.state = stateMenu
		return m, nil
	case "up", "k":
		if m.settings.fieldCursor > 0 {
			m.settings.fieldCursor--
		}
	case "down", "j":
		if m.settings.fieldCursor < 2 {
			m.settings.fieldCursor++
		}
	case "left", "h":
		m.settings.cycle(-1)
	case "right", "l":
		m.settings.cycle(1)
	case "enter":
		entry := gameRegistry[m.settings.gameIdx]
		m.activeGame = entry.NewWithStakes(m.settings.capital(), m.settings.ante(), m.settings.multiplier())
		m.state = statePlaying
		return m, m.activeGame.Init()
	}
	return m, nil
}

func (m AppModel) updatePlaying(msg tea.Msg) (tea.Model, tea.Cmd) {
	// Ctrl+C всегда выходит из всего приложения целиком, а не
	// только из текущей игры.
	if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == "ctrl+c" {
		m.quitting = true
		return m, tea.Quit
	}

	updated, cmd := m.activeGame.Update(msg)
	m.activeGame = updated

	backToMenu := false
	switch tm := updated.(type) {
	case Model:
		backToMenu = tm.backToMenu
	case DurakModel:
		backToMenu = tm.backToMenu
	case BlackjackModel:
		backToMenu = tm.backToMenu
	case OneOhOneModel:
		backToMenu = tm.backToMenu
	case ThousandModel:
		backToMenu = tm.backToMenu
	case KlondikeModel:
		backToMenu = tm.backToMenu
	case CheckersModel:
		backToMenu = tm.backToMenu
	case ChessModel:
		backToMenu = tm.backToMenu
	case CorpseStarchModel:
		backToMenu = tm.backToMenu
	case GoModel:
		backToMenu = tm.backToMenu
	case MinesweeperModel:
		backToMenu = tm.backToMenu
	case WayfarerModel:
		backToMenu = tm.backToMenu
	case TicTacToeModel:
		backToMenu = tm.backToMenu
	case NardyModel:
		backToMenu = tm.backToMenu
	case SenetModel:
		backToMenu = tm.backToMenu
	case UrModel:
		backToMenu = tm.backToMenu
	case PokerModel:
		backToMenu = tm.backToMenu
	}
	if backToMenu {
		m.state = stateMenu
		m.activeGame = nil
		return m, nil
	}
	return m, cmd
}

func (m AppModel) View() string {
	if m.quitting {
		return T("common.goodbye")
	}
	switch m.state {
	case statePlaying:
		return m.activeGame.View()
	case stateLanguage:
		return m.viewLanguage()
	case stateRulesMenu:
		return m.viewRulesMenu()
	case stateRules:
		return m.viewRules()
	case stateSettings:
		return m.viewSettings()
	case statePlayerCount:
		return m.viewPlayerCount()
	case stateBlackjackSettings:
		return m.viewBlackjackSettings()
	case stateCheckersSettings:
		return m.viewCheckersSettings()
	case stateTicTacToeSettings:
		return m.viewTicTacToeSettings()
	case stateNardySettings:
		return m.viewNardySettings()
	case stateUrSettings:
		return m.viewUrSettings()
	case stateSenetSettings:
		return m.viewSenetSettings()
	case stateChessSettings:
		return m.viewChessSettings()
	case stateGoSettings:
		return m.viewGoSettings()
	case stateMinesweeperSettings:
		return m.viewMinesweeperSettings()
	default:
		return m.viewMenu()
	}
}

func (m AppModel) viewLanguage() string {
	var b strings.Builder
	fmt.Fprintln(&b, menuTitleStyle.Render(T("lang.title")))
	fmt.Fprintln(&b)

	options := []string{T("lang.ru"), T("lang.en")}
	for i, opt := range options {
		cursor := "  "
		label := menuItemStyle.Render(opt)
		if i == m.langCursor {
			cursor = menuCursorStyle.Render("> ")
			label = menuCursorStyle.Render(opt)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, label)
	}
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("lang.hint")))
	return b.String()
}

func (m AppModel) viewMenu() string {
	var b strings.Builder
	if m.menuUsesCompactBanner() {
		fmt.Fprintln(&b, menuTitleStyle.Render(menuBannerCompact))
	} else {
		fmt.Fprintln(&b, menuTitleStyle.Render(asciiBanner))
	}
	fmt.Fprintln(&b, menuSubtitleStyle.Render(T("app.subtitle")))
	fmt.Fprintln(&b)

	visible := m.menuVisibleItems()
	start := m.menuScroll
	end := start + visible
	if end > len(m.options) {
		end = len(m.options)
	}

	if start > 0 {
		fmt.Fprintln(&b, dimStyle.Render("  ▲"))
	}
	for i := start; i < end; i++ {
		opt := m.options[i]
		cursor := "  "
		label := menuItemStyle.Render(opt.label())
		if i == m.cursor {
			cursor = menuCursorStyle.Render("> ")
			label = menuCursorStyle.Render(opt.label())
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, label)
		if d := opt.desc(); d != "" {
			wrapped := wrapText(d, m.termWidth)
			indented := strings.ReplaceAll(wrapped, "\n", "\n    ")
			fmt.Fprintf(&b, "    %s\n", menuDescStyle.Render(indented))
		}
	}
	if end < len(m.options) {
		fmt.Fprintln(&b, dimStyle.Render("  ▼"))
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("menu.hint")))
	return b.String()
}

func (m AppModel) viewRulesMenu() string {
	var b strings.Builder
	fmt.Fprintln(&b, menuTitleStyle.Render(T("rulesmenu.title")))
	fmt.Fprintln(&b)

	visible := m.rulesMenuVisibleItems()
	start := m.rulesMenuScroll
	end := start + visible
	if end > len(gameRegistry) {
		end = len(gameRegistry)
	}

	if start > 0 {
		fmt.Fprintln(&b, dimStyle.Render("  ▲"))
	}
	for i := start; i < end; i++ {
		entry := gameRegistry[i]
		cursor := "  "
		label := menuItemStyle.Render(entry.Name())
		if i == m.rulesCursor {
			cursor = menuCursorStyle.Render("> ")
			label = menuCursorStyle.Render(entry.Name())
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, label)
	}
	if end < len(gameRegistry) {
		fmt.Fprintln(&b, dimStyle.Render("  ▼"))
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("rulesmenu.hint")))
	return b.String()
}

func (m AppModel) viewRules() string {
	var b strings.Builder
	fmt.Fprintln(&b, menuTitleStyle.Render(T("rules.title")))
	fmt.Fprintln(&b)

	lines := strings.Split(m.rulesText, "\n")
	visible := m.rulesVisibleLines()
	start := m.rulesScroll
	if start > len(lines) {
		start = len(lines)
	}
	end := start + visible
	if end > len(lines) {
		end = len(lines)
	}
	fmt.Fprintln(&b, strings.Join(lines[start:end], "\n"))

	maxScroll := m.rulesMaxScroll()
	scrollInfo := ""
	if maxScroll > 0 {
		scrollInfo = "  " + Tf("rules.scroll_info", start+1, end, len(lines))
	}
	fmt.Fprintln(&b, menuHintStyle.Render(T("rules.hint")+scrollInfo))
	return b.String()
}

func (m AppModel) viewSettings() string {
	var b strings.Builder
	entry := gameRegistry[m.settings.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("stake.title"), entry.Name())))
	fmt.Fprintln(&b)

	fields := []struct {
		label string
		value string
	}{
		{T("stake.capital"), fmt.Sprintf("%d", m.settings.capital())},
		{T("stake.ante"), fmt.Sprintf("%d", m.settings.ante())},
		{T("stake.tonk_mult"), fmt.Sprintf("×%d", m.settings.multiplier())},
	}
	for i, f := range fields {
		cursor := "  "
		line := fmt.Sprintf("%-28s  ← %s →", f.label, f.value)
		if i == m.settings.fieldCursor {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	pot := m.settings.ante() * 4
	fmt.Fprintf(&b, "%s\n", menuDescStyle.Render(fmt.Sprintf(
		"%s: %d.  %s: ×%d = %d.",
		T("stake.pot_info"), pot, T("stake.tonk_win"), m.settings.multiplier(), pot*m.settings.multiplier())))

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("stake.hint")))
	return b.String()
}

func (m AppModel) viewPlayerCount() string {
	var b strings.Builder
	entry := gameRegistry[m.playerCount.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("playercount.title"), entry.Name())))
	fmt.Fprintln(&b)
	fmt.Fprintf(&b, "%s\n", menuCursorStyle.Render(fmt.Sprintf("  ← %d %s →", m.playerCount.count, T("playercount.line"))))
	fmt.Fprintln(&b)
	fmt.Fprintf(&b, "%s\n", menuDescStyle.Render(Tf("playercount.bots", m.playerCount.count-1)))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("playercount.hint")))
	return b.String()
}

func (m AppModel) viewBlackjackSettings() string {
	var b strings.Builder
	entry := gameRegistry[m.blackjack.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("bjsettings.title"), entry.Name())))
	fmt.Fprintln(&b)

	fields := []struct {
		label string
		value string
	}{
		{T("bjsettings.players"), fmt.Sprintf("%d", m.blackjack.playerCount)},
		{T("bjsettings.capital"), fmt.Sprintf("%d", m.blackjack.capital())},
		{T("bjsettings.bet"), fmt.Sprintf("%d", m.blackjack.bet())},
	}
	for i, f := range fields {
		cursor := "  "
		line := fmt.Sprintf("%-30s  ← %s →", f.label, f.value)
		if i == m.blackjack.fieldCursor {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	botsCount := m.blackjack.playerCount - 1
	if botsCount > 0 {
		fmt.Fprintf(&b, "%s\n", menuDescStyle.Render(Tf("bjsettings.multi", botsCount)))
	} else {
		fmt.Fprintln(&b, menuDescStyle.Render(T("bjsettings.solo")))
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("bjsettings.hint")))
	return b.String()
}

func (m AppModel) viewCheckersSettings() string {
	var b strings.Builder
	entry := gameRegistry[m.checkers.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("checkerslvl.title"), entry.Name())))
	fmt.Fprintln(&b)

	lvl := checkersLevels[m.checkers.levelIdx]
	fields := []struct{ label, value string }{
		{T("checkerslvl.field_label"), T(lvl.LabelKey)},
		{T("color.field_label"), colorChoiceLabel(m.checkers.colorChoice, "color.white", "color.black")},
	}
	for i, f := range fields {
		cursor := "  "
		line := fmt.Sprintf("%-30s  ← %s →", f.label, f.value)
		if i == m.checkers.fieldCursor {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, dimStyle.Render(T(lvl.DescKey)))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("gosettings.hint")))
	return b.String()
}

func (m AppModel) viewTicTacToeSettings() string {
	var b strings.Builder
	entry := gameRegistry[m.ticTacToe.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("tictactoelvl.title"), entry.Name())))
	fmt.Fprintln(&b)

	for i, lvl := range ticTacToeLevels {
		cursor := "  "
		line := fmt.Sprintf("%s (%s)", T(lvl.LabelKey), T(lvl.DescKey))
		if i == m.ticTacToe.levelIdx {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("checkerslvl.hint")))
	return b.String()
}

func (m AppModel) viewNardySettings() string {
	var b strings.Builder
	entry := gameRegistry[m.nardy.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("nardysettings.title"), entry.Name())))
	fmt.Fprintln(&b)

	fields := []struct {
		label string
		value string
	}{
		{T("nardysettings.variant"), T(nardyVariantOptions[m.nardy.variantIdx].LabelKey)},
		{T("nardysettings.difficulty"), T(nardyDifficultyLevels[m.nardy.difficultyIdx].LabelKey)},
		{T("color.field_label"), colorChoiceLabel(m.nardy.colorChoice, "color.white", "color.black")},
	}
	for i, f := range fields {
		cursor := "  "
		line := fmt.Sprintf("%-30s  ← %s →", f.label, f.value)
		if i == m.nardy.fieldCursor {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	desc := nardyDifficultyLevels[m.nardy.difficultyIdx].DescKey
	fmt.Fprintln(&b, dimStyle.Render(T(desc)))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("gosettings.hint")))
	return b.String()
}

func (m AppModel) viewUrSettings() string {
	var b strings.Builder
	entry := gameRegistry[m.ur.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("checkerslvl.title"), entry.Name())))
	fmt.Fprintln(&b)

	lvl := urDifficultyLevels[m.ur.levelIdx]
	fields := []struct{ label, value string }{
		{T("checkerslvl.field_label"), T(lvl.LabelKey)},
		{T("color.field_label"), colorChoiceLabel(m.ur.colorChoice, "color.white", "color.black")},
	}
	for i, f := range fields {
		cursor := "  "
		line := fmt.Sprintf("%-30s  ← %s →", f.label, f.value)
		if i == m.ur.fieldCursor {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, dimStyle.Render(T(lvl.DescKey)))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("gosettings.hint")))
	return b.String()
}

func (m AppModel) viewSenetSettings() string {
	var b strings.Builder
	entry := gameRegistry[m.senet.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("checkerslvl.title"), entry.Name())))
	fmt.Fprintln(&b)

	lvl := senetDifficultyLevels[m.senet.levelIdx]
	fields := []struct{ label, value string }{
		{T("checkerslvl.field_label"), T(lvl.LabelKey)},
		{T("color.field_label"), colorChoiceLabel(m.senet.colorChoice, "color.white", "color.black")},
	}
	for i, f := range fields {
		cursor := "  "
		line := fmt.Sprintf("%-30s  ← %s →", f.label, f.value)
		if i == m.senet.fieldCursor {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, dimStyle.Render(T(lvl.DescKey)))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("gosettings.hint")))
	return b.String()
}

func (m AppModel) viewChessSettings() string {
	var b strings.Builder
	entry := gameRegistry[m.chess.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("checkerslvl.title"), entry.Name())))
	fmt.Fprintln(&b)

	lvl := chessLevels[m.chess.levelIdx]
	fields := []struct{ label, value string }{
		{T("checkerslvl.field_label"), T(lvl.LabelKey)},
		{T("color.field_label"), colorChoiceLabel(m.chess.colorChoice, "color.white", "color.black")},
	}
	for i, f := range fields {
		cursor := "  "
		line := fmt.Sprintf("%-30s  ← %s →", f.label, f.value)
		if i == m.chess.fieldCursor {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, dimStyle.Render(T(lvl.DescKey)))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("gosettings.hint")))
	return b.String()
}

func (m AppModel) viewGoSettings() string {
	var b strings.Builder
	entry := gameRegistry[m.goSetup.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("gosettings.title"), entry.Name())))
	fmt.Fprintln(&b)

	fields := []struct {
		label string
		value string
	}{
		{T("gosettings.boardsize"), fmt.Sprintf("%dx%d", m.goSetup.size(), m.goSetup.size())},
		{T("gosettings.difficulty"), T(goDifficultyLevels[m.goSetup.difficultyIdx].LabelKey)},
		{T("color.field_label"), colorChoiceLabel(m.goSetup.colorChoice, "color.black", "color.white")},
	}
	for i, f := range fields {
		cursor := "  "
		line := fmt.Sprintf("%-30s  ← %s →", f.label, f.value)
		if i == m.goSetup.fieldCursor {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("gosettings.hint")))
	return b.String()
}

func (m AppModel) viewMinesweeperSettings() string {
	var b strings.Builder
	entry := gameRegistry[m.minesweeper.gameIdx]
	fmt.Fprintln(&b, menuTitleStyle.Render(fmt.Sprintf("=== %s: %s ===", T("mssettings.title"), entry.Name())))
	fmt.Fprintln(&b)

	for i, p := range minesweeperPresets {
		cursor := "  "
		line := fmt.Sprintf("%s — %dx%d, %d 💣", T(p.LabelKey), p.Width, p.Height, p.Mines)
		if i == m.minesweeper.presetIdx {
			cursor = menuCursorStyle.Render("> ")
			line = menuCursorStyle.Render(line)
		}
		fmt.Fprintf(&b, "%s%s\n", cursor, line)
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuHintStyle.Render(T("mssettings.hint")))
	return b.String()
}
