package main

import (
	"fmt"
	"math/rand"
	"sort"
	"strings"
	"time"

	tea "github.com/charmbracelet/bubbletea"
)

// wayfarerView — текущий экран игры.
type wayfarerView int

const (
	wfViewHub wayfarerView = iota
	wfViewMarket
	wfViewShipyard
	wfViewTravel
	// wfViewGalacticCoords — ввод координат (галактика + индекс
	// системы) для межгалактического прыжка вместо случайной цели.
	wfViewGalacticCoords
	wfViewCombat
	wfViewQuestList
	wfViewQuestStep
	wfViewQuestOffer
	wfViewGameOver
)

// wayfarerRefuelPrice / wayfarerRepairPrice — цены за единицу
// топлива/прочности корпуса на верфи (фиксированные — свои для
// каждой станции цены усложнили бы UI без особой пользы).
const (
	wayfarerRefuelPrice = 4.0
	wayfarerRepairPrice = 6.0

	wayfarerRegularMissilePrice = 150.0
	wayfarerHomingMissilePrice  = 300.0
	wayfarerDecoyPrice          = 80.0
)

// wayfarerEquipmentCost — стоимость разового приобретения снаряжения.
var wayfarerEquipmentCost = map[WayfarerEquipment]float64{
	EquipFuelScoop:      800,
	EquipCargoExpansion: 1200,
	EquipShieldBooster:  1500,
	EquipEscapePod:      2000,
	EquipBetterLaser:    1800,
}

var wayfarerEquipmentNameKey = map[WayfarerEquipment]string{
	EquipFuelScoop:      "wf.equip.fuelscoop",
	EquipCargoExpansion: "wf.equip.cargoexp",
	EquipShieldBooster:  "wf.equip.shieldbooster",
	EquipEscapePod:      "wf.equip.escapepod",
	EquipBetterLaser:    "wf.equip.betterlaser",
}

var wayfarerEquipmentOrder = []WayfarerEquipment{
	EquipFuelScoop, EquipCargoExpansion, EquipShieldBooster, EquipEscapePod, EquipBetterLaser,
}

// wayfarerStartSystem — фиксированная стартовая система новой
// карьеры (детерминированно — как "Lave" в классической Elite).
var wayfarerStartSystem = WayfarerSystemID{Galaxy: 0, Index: 0}

// WayfarerModel — TUI поверх WayfarerShip.
type WayfarerModel struct {
	ship *WayfarerShip
	view wayfarerView

	combat              *WayfarerCombatState
	combatRewardCredits float64 // награда за успешный бой/сигнал бедствия, если применимо

	// combatDistributing / distFore/Aft/Sides — подрежим боя, в
	// котором игрок временно настраивает распределение энергии щита
	// по трём направлениям перед подтверждением (см.
	// handleShieldDistributionKey). Раунд тратится только при
	// подтверждении, не при входе в сам подрежим.
	combatDistributing bool
	distFocus          int // 0=нос, 1=хвост, 2=левый борт, 3=правый борт — какое направление сейчас регулируется
	distFore           int
	distAft            int
	distLeft           int
	distRight          int

	// combatChoosingFacing / pendingAttackWeapon — подрежим выбора
	// направления удара по врагу после того, как игрок уже выбрал
	// оружие (см. combatActions/handleFacingChoiceKey).
	combatChoosingFacing bool
	pendingAttackWeapon  WayfarerWeaponType

	cursor int // курсор для рынка/верфи/перелёта/квестов (что уместно в текущем view)

	travelCandidates []WayfarerSystemID

	// galacticTargetGalaxy / galacticTargetIndex — вводимые игроком
	// координаты цели межгалактического прыжка (см. wfViewGalacticCoords).
	galacticTargetGalaxy int
	galacticTargetIndex  int

	pendingQuestKey string // ключ квеста, предложенного встречей в пути, но ещё не принятого/отклонённого

	marketPrices []float64 // цены на товары в текущей системе — считаются один раз при стыковке, не на каждый рендер (см. rollMarketPrices)

	confirmingReset bool // ждём подтверждения "начать новую карьеру" (y/n) — необратимое действие

	message string
	isError bool

	rng *rand.Rand

	termWidth int // для переноса длинного текста квестов/сообщений по ширине реального терминала

	quitting   bool
	backToMenu bool
}

// NewWayfarerModel загружает сохранённую карьеру, если она есть, или
// начинает новую с фиксированной стартовой системы.
func NewWayfarerModel() WayfarerModel {
	rng := rand.New(rand.NewSource(time.Now().UnixNano()))
	var m WayfarerModel
	if ship, err := LoadWayfarerShip(); err == nil {
		m = WayfarerModel{ship: ship, view: wfViewHub, rng: rng, termWidth: 80}
	} else {
		m = WayfarerModel{ship: NewWayfarerShip(wayfarerStartSystem), view: wfViewHub, rng: rng, termWidth: 80}
	}
	m.rollMarketPrices()
	return m
}

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

func (m *WayfarerModel) setInfo(key string) {
	m.message = T(key)
	m.isError = false
}

func (m *WayfarerModel) setInfof(key string, args ...interface{}) {
	m.message = Tf(key, args...)
	m.isError = false
}

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

func (m WayfarerModel) currentSystem() WayfarerSystem {
	return GenerateSystem(m.ship.CurrentSystem)
}

// rollMarketPrices пересчитывает цены на товары в текущей системе.
// Вызывается один раз при стыковке (старт игры, загрузка карьеры,
// успешный прыжок) — а не при каждом рендере рынка, иначе цены
// мелькали бы на каждое нажатие клавиши, даже просто при движении
// курсора по списку товаров.
func (m *WayfarerModel) rollMarketPrices() {
	sys := m.currentSystem()
	m.marketPrices = make([]float64, len(WayfarerCommodities))
	for i := range WayfarerCommodities {
		m.marketPrices[i] = CommodityPrice(i, sys, m.rng)
	}
}

func (m WayfarerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	if sizeMsg, ok := msg.(tea.WindowSizeMsg); ok {
		m.termWidth = sizeMsg.Width
		return m, nil
	}
	keyMsg, ok := msg.(tea.KeyMsg)
	if !ok {
		return m, nil
	}
	return m.handleKey(keyMsg)
}

func (m WayfarerModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
	key := msg.String()
	if key == "ctrl+c" {
		_ = SaveWayfarerShip(m.ship)
		m.quitting = true
		return m, tea.Quit
	}

	if m.confirmingReset {
		switch key {
		case "y", "Y":
			_ = DeleteWayfarerSave()
			m.ship = NewWayfarerShip(wayfarerStartSystem)
			m.confirmingReset = false
			m.combat = nil
			m.pendingQuestKey = ""
			m.view = wfViewHub
			m.rollMarketPrices()
			m.setInfo("wf.msg.reset_done")
		case "n", "N", "esc":
			m.confirmingReset = false
			m.message = ""
		}
		return m, nil
	}

	switch m.view {
	case wfViewHub:
		return m.handleHubKey(key)
	case wfViewMarket:
		return m.handleMarketKey(key)
	case wfViewShipyard:
		return m.handleShipyardKey(key)
	case wfViewTravel:
		return m.handleTravelKey(key)
	case wfViewGalacticCoords:
		return m.handleGalacticCoordsKey(key)
	case wfViewCombat:
		return m.handleCombatKey(key)
	case wfViewQuestList:
		return m.handleQuestListKey(key)
	case wfViewQuestStep:
		return m.handleQuestStepKey(key)
	case wfViewQuestOffer:
		return m.handleQuestOfferKey(key)
	case wfViewGameOver:
		return m.handleGameOverKey(key)
	}
	return m, nil
}

// --- Хаб станции -----------------------------------------------------

func (m WayfarerModel) handleHubKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "q":
		_ = SaveWayfarerShip(m.ship)
		m.backToMenu = true
		return m, nil
	case "1":
		m.view = wfViewMarket
		m.cursor = 0
		m.message = ""
	case "2":
		m.view = wfViewShipyard
		m.cursor = 0
		m.message = ""
	case "3":
		m.travelCandidates = wayfarerNearbySystems(m.ship.CurrentSystem, m.ship.FuelRange())
		m.view = wfViewTravel
		m.cursor = 0
		m.message = ""
	case "4":
		m.view = wfViewQuestList
		m.cursor = 0
		m.message = ""
	case "N":
		m.confirmingReset = true
	}
	return m, nil
}

// --- Рынок -------------------------------------------------------------

func (m WayfarerModel) handleMarketKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "q", "esc":
		m.view = wfViewHub
		m.message = ""
		return m, nil
	case "up", "k":
		if m.cursor > 0 {
			m.cursor--
		}
	case "down", "j":
		if m.cursor < len(WayfarerCommodities)-1 {
			m.cursor++
		}
	case "b", "s", "B", "S":
		price := m.marketPrices[m.cursor]
		switch key {
		case "b":
			if err := m.ship.BuyCommodity(m.cursor, 1, price); err != nil {
				m.setError(err)
			} else {
				m.setInfof("wf.msg.bought", 1, T(WayfarerCommodities[m.cursor].Key))
			}
		case "B":
			qty := m.ship.CargoFree()
			affordable := int(m.ship.Credits / price)
			if affordable < qty {
				qty = affordable
			}
			if err := m.ship.BuyCommodity(m.cursor, qty, price); err != nil {
				m.setError(err)
			} else {
				m.setInfof("wf.msg.bought", qty, T(WayfarerCommodities[m.cursor].Key))
			}
		case "s":
			sellPrice := SellPrice(price)
			if err := m.ship.SellCommodity(m.cursor, 1, sellPrice); err != nil {
				m.setError(err)
			} else {
				m.setInfof("wf.msg.sold", 1, T(WayfarerCommodities[m.cursor].Key))
			}
		case "S":
			sellPrice := SellPrice(price)
			qty := m.ship.Cargo[m.cursor]
			if err := m.ship.SellCommodity(m.cursor, qty, sellPrice); err != nil {
				m.setError(err)
			} else {
				m.setInfof("wf.msg.sold", qty, T(WayfarerCommodities[m.cursor].Key))
			}
		}
	}
	return m, nil
}

// --- Верфь ---------------------------------------------------------------

func (m WayfarerModel) shipyardActionCount() int {
	return 5 + len(wayfarerEquipmentOrder) // дозаправка + ремонт + 2 вида ракет + ловушки + снаряжение
}

func (m WayfarerModel) handleShipyardKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "q", "esc":
		m.view = wfViewHub
		m.message = ""
		return m, nil
	case "up", "k":
		if m.cursor > 0 {
			m.cursor--
		}
	case "down", "j":
		if m.cursor < m.shipyardActionCount()-1 {
			m.cursor++
		}
	case "enter", " ":
		switch {
		case m.cursor == 0:
			need := m.ship.FuelCapacity - m.ship.Fuel
			if err := m.ship.Refuel(need, wayfarerRefuelPrice); err != nil {
				m.setError(err)
			} else {
				m.setInfo("wf.msg.refueled")
			}
		case m.cursor == 1:
			need := m.ship.MaxHull - m.ship.Hull
			if err := m.ship.Repair(need, wayfarerRepairPrice); err != nil {
				m.setError(err)
			} else {
				m.setInfo("wf.msg.repaired")
			}
		case m.cursor == 2:
			if err := m.ship.BuyMissile(WayfarerWeaponMissileRegular, wayfarerRegularMissilePrice); err != nil {
				m.setError(err)
			} else {
				m.setInfo("wf.msg.missile_bought")
			}
		case m.cursor == 3:
			if err := m.ship.BuyMissile(WayfarerWeaponMissileHoming, wayfarerHomingMissilePrice); err != nil {
				m.setError(err)
			} else {
				m.setInfo("wf.msg.missile_bought")
			}
		case m.cursor == 4:
			if err := m.ship.BuyDecoy(wayfarerDecoyPrice); err != nil {
				m.setError(err)
			} else {
				m.setInfo("wf.msg.decoy_bought")
			}
		default:
			eq := wayfarerEquipmentOrder[m.cursor-5]
			if m.ship.Equipment[eq] {
				m.setError(errWayfarerAlreadyOwned)
				return m, nil
			}
			cost := wayfarerEquipmentCost[eq]
			if m.ship.Credits < cost {
				m.setError(errWayfarerInsufficientFunds)
				return m, nil
			}
			m.ship.Credits -= cost
			m.ship.Equipment[eq] = true
			applyWayfarerEquipmentEffect(m.ship, eq)
			m.setInfo("wf.msg.equipped")
		}
	}
	return m, nil
}

// applyWayfarerEquipmentEffect применяет однократный эффект покупки
// снаряжения к статам корабля (там, где это применимо — щитовой
// бустер и улучшенный лазер сразу повышают соответствующие статы).
func applyWayfarerEquipmentEffect(s *WayfarerShip, eq WayfarerEquipment) {
	switch eq {
	case EquipShieldBooster:
		s.MaxShield += 40
		s.Shield += 40
	case EquipBetterLaser:
		s.LaserPower += 8
	case EquipCargoExpansion:
		s.CargoCapacity += 15
	case EquipFuelScoop:
		s.FuelCapacity += 3
		s.Fuel += 3
	}
}

var errWayfarerAlreadyOwned = wayfarerErr("это снаряжение уже установлено")

// --- Перелёт ---------------------------------------------------------------

// wayfarerNearbySystems находит до 9 ближайших систем в пределах
// maxRange световых лет от current (в той же галактике), плюс —
// отдельным пунктом в конце списка — возможность гиперпрыжка в
// следующую галактику (см. render/handle travel).
func wayfarerNearbySystems(current WayfarerSystemID, maxRange float64) []WayfarerSystemID {
	from := GenerateSystem(current)
	type candidate struct {
		id   WayfarerSystemID
		dist float64
	}
	var candidates []candidate
	for i := 0; i < WayfarerSystemsPerGalaxy; i++ {
		if i == current.Index {
			continue
		}
		id := WayfarerSystemID{Galaxy: current.Galaxy, Index: i}
		dist := from.Distance(GenerateSystem(id))
		if dist <= maxRange {
			candidates = append(candidates, candidate{id, dist})
		}
	}
	sort.Slice(candidates, func(i, j int) bool { return candidates[i].dist < candidates[j].dist })
	if len(candidates) > 9 {
		candidates = candidates[:9]
	}
	out := make([]WayfarerSystemID, len(candidates))
	for i, c := range candidates {
		out[i] = c.id
	}
	return out
}

func (m WayfarerModel) handleTravelKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "q", "esc":
		m.view = wfViewHub
		m.message = ""
		return m, nil
	case "up", "k":
		if m.cursor > 0 {
			m.cursor--
		}
	case "down", "j":
		max := len(m.travelCandidates) // +1 пункт межгалактического прыжка обрабатывается отдельным индексом
		if m.cursor < max {
			m.cursor++
		}
	case "enter", " ":
		if m.cursor == len(m.travelCandidates) {
			m.galacticTargetGalaxy = (m.ship.CurrentSystem.Galaxy + 1) % WayfarerGalaxyCount
			m.galacticTargetIndex = 0
			m.view = wfViewGalacticCoords
			return m, nil
		}
		if m.cursor >= len(m.travelCandidates) {
			return m, nil
		}
		target := m.travelCandidates[m.cursor]
		return m.performJump(target)
	}
	return m, nil
}

// handleGalacticCoordsKey обрабатывает ввод координат (галактика +
// индекс системы) на экране выбора цели межгалактического прыжка.
func (m WayfarerModel) handleGalacticCoordsKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "q", "esc":
		m.view = wfViewTravel
		m.message = ""
		return m, nil
	case "left", "h":
		m.galacticTargetGalaxy--
		if m.galacticTargetGalaxy < 0 {
			m.galacticTargetGalaxy = WayfarerGalaxyCount - 1
		}
	case "right", "l":
		m.galacticTargetGalaxy = (m.galacticTargetGalaxy + 1) % WayfarerGalaxyCount
	case "up", "k":
		m.galacticTargetIndex++
		if m.galacticTargetIndex >= WayfarerSystemsPerGalaxy {
			m.galacticTargetIndex = 0
		}
	case "down", "j":
		m.galacticTargetIndex--
		if m.galacticTargetIndex < 0 {
			m.galacticTargetIndex = WayfarerSystemsPerGalaxy - 1
		}
	case "shift+up":
		m.galacticTargetIndex = (m.galacticTargetIndex + 10) % WayfarerSystemsPerGalaxy
	case "shift+down":
		m.galacticTargetIndex -= 10
		if m.galacticTargetIndex < 0 {
			m.galacticTargetIndex += WayfarerSystemsPerGalaxy
		}
	case "enter", " ":
		return m.performGalacticJump(WayfarerSystemID{Galaxy: m.galacticTargetGalaxy, Index: m.galacticTargetIndex})
	}
	return m, nil
}

// performGalacticJump — межгалактический прыжок в явно выбранные
// игроком координаты (галактика + индекс системы). Требует почти
// полного бака — платит собой весь текущий запас топлива.
func (m WayfarerModel) performGalacticJump(target WayfarerSystemID) (tea.Model, tea.Cmd) {
	if m.ship.Fuel < m.ship.FuelCapacity*0.9 {
		m.setError(errWayfarerNeedFullTank)
		return m, nil
	}
	if target.Galaxy == m.ship.CurrentSystem.Galaxy {
		m.setError(errWayfarerSameGalaxy)
		return m, nil
	}
	m.ship.Fuel = 0
	m.ship.CurrentSystem = target
	m.ship.Visited[target] = true
	m.rollMarketPrices()
	m.setInfo("wf.msg.galactic_jump")
	m.view = wfViewHub
	return m, nil
}

var errWayfarerNeedFullTank = wayfarerErr("для межгалактического прыжка нужен полный (или почти полный) бак")
var errWayfarerSameGalaxy = wayfarerErr("межгалактический прыжок должен вести в другую галактику")

// performJump выполняет обычный прыжок и разрешает случайную
// встречу в пути, если она произошла.
func (m WayfarerModel) performJump(target WayfarerSystemID) (tea.Model, tea.Cmd) {
	if err := m.ship.JumpTo(target); err != nil {
		m.setError(err)
		return m, nil
	}
	m.rollMarketPrices()

	dest := GenerateSystem(target)
	encounter := RollEncounter(dest, m.rng)
	switch encounter {
	case EncounterNone:
		if questKey, ok := RollQuestOffer(m.ship, m.rng); ok {
			m.pendingQuestKey = questKey
			m.view = wfViewQuestOffer
			m.message = ""
			return m, nil
		}
		m.setInfo("wf.msg.jump_quiet")
		m.view = wfViewHub
	case EncounterPirate:
		hull, laser, shield := GeneratePirateStats(dest, m.rng)
		m.combat = NewWayfarerCombat("wf.enemy.pirate", hull, laser,
			m.ship.Hull, m.ship.MaxHull, m.ship.Shield, m.ship.MaxShield, m.ship.LaserPower, true, randomEnemyWeapon(m.rng), shield, m.rng)
		m.combatRewardCredits = float64(hull) * 4
		m.view = wfViewCombat
		m.setInfo("wf.msg.pirate_ambush")
	case EncounterPoliceScan:
		if m.ship.HasIllegalCargo() {
			lost := m.ship.ConfiscateIllegalCargo()
			m.setInfof("wf.msg.cargo_confiscated", formatWfCredits(lost))
		} else {
			m.setInfo("wf.msg.scan_clear")
		}
		m.view = wfViewHub
	case EncounterDistress:
		reward, wasTrap := m.ship.HelpDistressed(m.rng)
		if wasTrap {
			hull, laser, shield := GeneratePirateStats(dest, m.rng)
			m.combat = NewWayfarerCombat("wf.enemy.raider", hull, laser,
				m.ship.Hull, m.ship.MaxHull, m.ship.Shield, m.ship.MaxShield, m.ship.LaserPower, true, randomEnemyWeapon(m.rng), shield, m.rng)
			m.combatRewardCredits = float64(hull) * 4
			m.view = wfViewCombat
			m.setInfo("wf.msg.distress_trap")
		} else {
			m.setInfof("wf.msg.distress_reward", formatWfCredits(reward))
			m.view = wfViewHub
		}
	case EncounterDerelict:
		found := m.ship.DerelictFind(m.rng)
		m.setInfof("wf.msg.derelict_find", formatWfCredits(found))
		m.view = wfViewHub
	}
	return m, nil
}

// --- Бой -------------------------------------------------------------------

// wayfarerCombatAction — один пункт динамического списка боевых
// действий (какие именно доступны — зависит от боезапаса, наличия
// ловушек, типа оружия врага).
type wayfarerCombatAction struct {
	Label  string
	Handle func(m *WayfarerModel)
}

// combatActions строит список доступных сейчас боевых действий.
// Порядок: атака лазером всегда первая, дальше — ракеты (если есть
// боезапас), бегство, переговоры (если применимо), уклонение (только
// против обычной ракеты врага), тепловая ловушка (только против
// самонаводящейся, и если ловушки есть), и распределение щитов —
// последним, всегда доступно. Выбор атаки (лазер/ракета) не бьёт
// сразу — сначала открывает подрежим выбора направления удара по
// врагу (см. handleFacingChoiceKey).
func (m WayfarerModel) combatActions() []wayfarerCombatAction {
	c := m.combat
	var actions []wayfarerCombatAction

	actions = append(actions, wayfarerCombatAction{
		Label: T("wf.combat.action.attack_laser"),
		Handle: func(m *WayfarerModel) {
			m.combatChoosingFacing = true
			m.pendingAttackWeapon = WayfarerWeaponLaser
		},
	})
	if m.ship.RegularMissiles > 0 {
		actions = append(actions, wayfarerCombatAction{
			Label: Tf("wf.combat.action.attack_missile_regular", m.ship.RegularMissiles),
			Handle: func(m *WayfarerModel) {
				m.combatChoosingFacing = true
				m.pendingAttackWeapon = WayfarerWeaponMissileRegular
			},
		})
	}
	if m.ship.HomingMissiles > 0 {
		actions = append(actions, wayfarerCombatAction{
			Label: Tf("wf.combat.action.attack_missile_homing", m.ship.HomingMissiles),
			Handle: func(m *WayfarerModel) {
				m.combatChoosingFacing = true
				m.pendingAttackWeapon = WayfarerWeaponMissileHoming
			},
		})
	}
	actions = append(actions, wayfarerCombatAction{
		Label: T("wf.combat.action.flee"),
		Handle: func(m *WayfarerModel) {
			m.combat.Flee(m.rng)
		},
	})
	if c.Negotiable {
		actions = append(actions, wayfarerCombatAction{
			Label: T("wf.combat.action.negotiate"),
			Handle: func(m *WayfarerModel) {
				m.combat.Negotiate(m.rng)
			},
		})
	}
	if c.EnemyWeapon == WayfarerWeaponMissileRegular {
		actions = append(actions, wayfarerCombatAction{
			Label: T("wf.combat.action.dodge"),
			Handle: func(m *WayfarerModel) {
				m.combat.Dodge(m.rng)
			},
		})
	}
	if c.EnemyWeapon == WayfarerWeaponMissileHoming && m.ship.Decoys > 0 {
		actions = append(actions, wayfarerCombatAction{
			Label: Tf("wf.combat.action.decoy", m.ship.Decoys),
			Handle: func(m *WayfarerModel) {
				m.combat.UseDecoy(m.ship)
			},
		})
	}
	actions = append(actions, wayfarerCombatAction{
		Label: T("wf.combat.action.distribute"),
		Handle: func(m *WayfarerModel) {
			m.combatDistributing = true
			m.distFocus = 0
			m.distFore = m.combat.PlayerShieldFore
			m.distAft = m.combat.PlayerShieldAft
			m.distLeft = m.combat.PlayerShieldLeft
			m.distRight = m.combat.PlayerShieldRight
		},
	})
	return actions
}

func (m WayfarerModel) handleCombatKey(key string) (tea.Model, tea.Cmd) {
	c := m.combat
	if c == nil || c.Outcome != CombatOngoing {
		return m, nil
	}
	if m.combatDistributing {
		return m.handleShieldDistributionKey(key)
	}
	if m.combatChoosingFacing {
		return m.handleFacingChoiceKey(key)
	}
	actions := m.combatActions()
	for i, a := range actions {
		if key == fmt.Sprintf("%d", i+1) {
			a.Handle(&m)
			if m.combatDistributing || m.combatChoosingFacing {
				return m, nil // вошли в подрежим — раунд ещё не потрачен
			}
			return m.resolveCombatIfDone()
		}
	}
	return m, nil
}

// wayfarerFacingKeys — какой клавишей выбирается каждое из четырёх
// направлений при прицельной атаке.
var wayfarerFacingKeys = map[string]WayfarerShieldFacing{
	"1": WayfarerShieldFore,
	"2": WayfarerShieldAft,
	"3": WayfarerShieldLeft,
	"4": WayfarerShieldRight,
}

// handleFacingChoiceKey обрабатывает выбор направления удара по
// врагу после того, как игрок уже выбрал оружие (лазер/ракета) —
// см. combatActions. Выбор направления сразу же выполняет атаку.
func (m WayfarerModel) handleFacingChoiceKey(key string) (tea.Model, tea.Cmd) {
	if key == "esc" || key == "q" {
		m.combatChoosingFacing = false
		return m, nil
	}
	facing, ok := wayfarerFacingKeys[key]
	if !ok {
		return m, nil
	}
	m.combatChoosingFacing = false
	switch m.pendingAttackWeapon {
	case WayfarerWeaponLaser:
		m.combat.Attack(facing, m.rng)
	default:
		if err := m.combat.AttackWithMissile(m.ship, m.pendingAttackWeapon, facing, m.rng); err != nil {
			m.setError(err)
			return m, nil
		}
	}
	return m.resolveCombatIfDone()
}

// handleShieldDistributionKey обрабатывает ввод в подрежиме
// настройки распределения щита перед подтверждением.
func (m WayfarerModel) handleShieldDistributionKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "esc", "q":
		m.combatDistributing = false
		return m, nil
	case "left", "h":
		m.distFocus = (m.distFocus + 3) % 4
	case "right", "l":
		m.distFocus = (m.distFocus + 1) % 4
	case "up", "k":
		m.adjustDistFocus(5)
	case "down", "j":
		m.adjustDistFocus(-5)
	case "enter", " ":
		if err := m.combat.DistributeShields(m.distFore, m.distAft, m.distLeft, m.distRight, m.rng); err != nil {
			m.setError(err)
			return m, nil
		}
		m.combatDistributing = false
		return m.resolveCombatIfDone()
	}
	return m, nil
}

func (m *WayfarerModel) adjustDistFocus(delta int) {
	var target *int
	switch m.distFocus {
	case 0:
		target = &m.distFore
	case 1:
		target = &m.distAft
	case 2:
		target = &m.distLeft
	default:
		target = &m.distRight
	}
	*target += delta
	if *target < 0 {
		*target = 0
	}
	if *target > m.combat.PlayerMaxShield {
		*target = m.combat.PlayerMaxShield
	}
}

func (m WayfarerModel) resolveCombatIfDone() (tea.Model, tea.Cmd) {
	c := m.combat
	switch c.Outcome {
	case CombatWon:
		m.ship.Hull = c.PlayerHull
		m.ship.Shield = c.TotalShield()
		m.ship.Kills++
		m.ship.Credits += m.combatRewardCredits
		m.setInfof("wf.msg.combat_won", formatWfCredits(m.combatRewardCredits))
		m.view = wfViewHub
		m.combat = nil
	case CombatFled:
		m.ship.Hull = c.PlayerHull
		m.ship.Shield = c.TotalShield()
		m.setInfo("wf.msg.combat_fled")
		m.view = wfViewHub
		m.combat = nil
	case CombatNegotiated:
		m.ship.Hull = c.PlayerHull
		m.ship.Shield = c.TotalShield()
		m.setInfo("wf.msg.combat_negotiated")
		m.view = wfViewHub
		m.combat = nil
	case CombatLost:
		m.ship.Hull = 0
		if m.ship.Equipment[EquipEscapePod] {
			m.ship.Equipment[EquipEscapePod] = false // одноразовое использование
			m.ship.Hull = m.ship.MaxHull / 4
			m.ship.Shield = 0
			m.ship.Credits = m.ship.Credits / 2
			for i := range m.ship.Cargo {
				m.ship.Cargo[i] = 0
			}
			m.setInfo("wf.msg.escape_pod_used")
			m.view = wfViewHub
			m.combat = nil
		} else {
			m.view = wfViewGameOver
		}
	}
	return m, nil
}

// --- Список квестов -----------------------------------------------------

// availableQuests возвращает квесты, которые ещё можно предложить
// игроку (не завершены и не провалены) — используется только для
// подсчёта в статусе и функцией RollQuestOffer, свободного выбора
// из этого списка больше нет (квесты теперь предлагаются случайно
// при прибытии в систему).
func (m WayfarerModel) availableQuests() []WayfarerQuestDef {
	var out []WayfarerQuestDef
	for _, q := range WayfarerQuestDefs {
		if !m.ship.CompletedQuests[q.Key] && !m.ship.FailedQuests[q.Key] {
			out = append(out, q)
		}
	}
	return out
}

func (m WayfarerModel) handleQuestListKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "q", "esc":
		m.view = wfViewHub
		m.message = ""
	case "enter", " ":
		if m.ship.ActiveQuest != nil {
			m.view = wfViewQuestStep
		}
	}
	return m, nil
}

func (m WayfarerModel) handleQuestOfferKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "1", "enter", " ":
		questKey := m.pendingQuestKey
		m.pendingQuestKey = ""
		if err := m.ship.StartQuest(questKey); err != nil {
			m.setError(err)
			m.view = wfViewHub
			return m, nil
		}
		m.view = wfViewQuestStep
	case "2", "q", "esc":
		m.pendingQuestKey = ""
		m.setInfo("wf.quests.declined")
		m.view = wfViewHub
	}
	return m, nil
}

func (m WayfarerModel) viewQuestOffer() string {
	var b strings.Builder
	def, _ := FindWayfarerQuest(m.pendingQuestKey)
	fmt.Fprintln(&b, titleStyle.Render(T("wf.quests.offer.title")))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, menuCursorStyle.Render(T(def.TitleKey)))
	fmt.Fprintln(&b, wfWrap(T(def.BriefKey), m.termWidth))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, T("wf.quests.offer.accept"))
	fmt.Fprintln(&b, T("wf.quests.offer.decline"))
	return b.String()
}

func (m WayfarerModel) handleQuestStepKey(key string) (tea.Model, tea.Cmd) {
	step, ok := m.ship.CurrentQuestStep()
	if !ok {
		m.view = wfViewQuestList
		m.cursor = 0
		return m, nil
	}
	switch key {
	case "q", "esc":
		m.ship.AbandonQuest()
		m.view = wfViewQuestList
		m.cursor = 0
		return m, nil
	}
	for i := 1; i <= 9 && i <= len(step.Choices); i++ {
		if key == fmt.Sprintf("%d", i) {
			choice, err := m.ship.ChooseQuestOption(i - 1)
			if err != nil {
				m.setError(err)
				return m, nil
			}
			if choice.ResultKey != "" {
				m.message = T(choice.ResultKey)
				m.isError = false
			}
			if m.ship.ActiveQuest == nil {
				m.view = wfViewQuestList
				m.cursor = 0
			}
			return m, nil
		}
	}
	return m, nil
}

// --- Экран окончания карьеры (гибель без спасательной капсулы) --------

func (m WayfarerModel) handleGameOverKey(key string) (tea.Model, tea.Cmd) {
	switch key {
	case "n":
		_ = DeleteWayfarerSave()
		m.ship = NewWayfarerShip(wayfarerStartSystem)
		m.view = wfViewHub
		m.message = ""
		m.combat = nil
	case "q":
		_ = DeleteWayfarerSave()
		m.backToMenu = true
	}
	return m, nil
}

// --- Форматирование -----------------------------------------------------

func formatWfCredits(v float64) string {
	return fmt.Sprintf("%.0f", v)
}

// --- Отрисовка ------------------------------------------------------------

func (m WayfarerModel) View() string {
	if m.quitting {
		return T("common.goodbye")
	}
	if m.confirmingReset {
		var b strings.Builder
		fmt.Fprintln(&b, titleStyle.Render(T("wf.title")))
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, errorStyle.Render(T("wf.confirm_reset")))
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, dimStyle.Render(T("wf.confirm_reset.hint")))
		return b.String()
	}
	switch m.view {
	case wfViewMarket:
		return m.viewMarket()
	case wfViewShipyard:
		return m.viewShipyard()
	case wfViewTravel:
		return m.viewTravel()
	case wfViewGalacticCoords:
		return m.viewGalacticCoords()
	case wfViewCombat:
		return m.viewCombat()
	case wfViewQuestList:
		return m.viewQuestList()
	case wfViewQuestStep:
		return m.viewQuestStep()
	case wfViewQuestOffer:
		return m.viewQuestOffer()
	case wfViewGameOver:
		return m.viewGameOver()
	default:
		return m.viewHub()
	}
}

func (m WayfarerModel) renderShipStatus() string {
	s := m.ship
	return Tf("wf.status_line",
		formatWfCredits(s.Credits), s.CargoUsed(), s.CargoCapacity,
		s.Fuel, s.FuelCapacity, s.Hull, s.MaxHull, s.Shield, s.MaxShield)
}

// wfWrap — тонкая обёртка над общей wrapText (сохранена ради
// минимальных изменений во всех местах вызова в этом файле).
func wfWrap(text string, termWidth int) string {
	return wrapText(text, termWidth)
}

func (m WayfarerModel) renderMessage(b *strings.Builder) {
	if m.message == "" {
		return
	}
	fmt.Fprintln(b)
	if m.isError {
		fmt.Fprintln(b, errorStyle.Render(wfWrap("! "+m.message, m.termWidth)))
	} else {
		fmt.Fprintln(b, infoStyle.Render(wfWrap(m.message, m.termWidth)))
	}
}

func (m WayfarerModel) viewHub() string {
	sys := m.currentSystem()
	var b strings.Builder
	fmt.Fprintln(&b, titleStyle.Render(T("wf.title")))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, Tf("wf.system_line", sys.Name, T(wayfarerGovernmentNameKeys[sys.Government]), sys.TechLevel))
	fmt.Fprintln(&b, m.renderShipStatus())
	fmt.Fprintln(&b, Tf("wf.rating_line", T(m.ship.PilotRating()), m.ship.Kills))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, T("wf.hub.hint1"))
	fmt.Fprintln(&b, T("wf.hub.hint2"))
	m.renderMessage(&b)
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, dimStyle.Render(T("wf.hint.quit")))
	return b.String()
}

func (m WayfarerModel) viewMarket() string {
	var b strings.Builder
	fmt.Fprintln(&b, titleStyle.Render(T("wf.market.title")))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, m.renderShipStatus())
	fmt.Fprintln(&b)
	for i, c := range WayfarerCommodities {
		price := m.marketPrices[i]
		line := Tf("wf.market.line", T(c.Key), price, m.ship.Cargo[i])
		if m.ship.Cargo[i] > 0 {
			line += Tf("wf.market.bought_at", m.ship.CargoCostBasis[i])
		}
		if c.Illegal {
			line += " " + dimStyle.Render(T("wf.market.illegal_tag"))
		}
		if i == m.cursor {
			line = menuCursorStyle.Render("> " + line)
		} else {
			line = "  " + line
		}
		fmt.Fprintln(&b, line)
	}
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, T("wf.market.hint"))
	m.renderMessage(&b)
	return b.String()
}

func (m WayfarerModel) viewShipyard() string {
	var b strings.Builder
	fmt.Fprintln(&b, titleStyle.Render(T("wf.shipyard.title")))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, m.renderShipStatus())
	fmt.Fprintln(&b)

	lines := []string{
		Tf("wf.shipyard.refuel", (m.ship.FuelCapacity-m.ship.Fuel)*wayfarerRefuelPrice),
		Tf("wf.shipyard.repair", float64(m.ship.MaxHull-m.ship.Hull)*wayfarerRepairPrice),
		Tf("wf.shipyard.missile_regular", wayfarerRegularMissilePrice, m.ship.RegularMissiles, m.ship.MissilesLoaded(), m.ship.MissileCapacity),
		Tf("wf.shipyard.missile_homing", wayfarerHomingMissilePrice, m.ship.HomingMissiles, m.ship.MissilesLoaded(), m.ship.MissileCapacity),
		Tf("wf.shipyard.decoy", wayfarerDecoyPrice, m.ship.Decoys),
	}
	for _, eq := range wayfarerEquipmentOrder {
		status := T("wf.shipyard.available")
		if m.ship.Equipment[eq] {
			status = T("wf.shipyard.owned")
		}
		lines = append(lines, Tf("wf.shipyard.equip_line", T(wayfarerEquipmentNameKey[eq]), wayfarerEquipmentCost[eq], status))
	}
	for i, line := range lines {
		if i == m.cursor {
			fmt.Fprintln(&b, menuCursorStyle.Render("> "+line))
		} else {
			fmt.Fprintln(&b, "  "+line)
		}
	}
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, T("wf.shipyard.hint"))
	m.renderMessage(&b)
	return b.String()
}

func (m WayfarerModel) viewGalacticCoords() string {
	var b strings.Builder
	fmt.Fprintln(&b, titleStyle.Render(T("wf.galactic.title")))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, Tf("wf.galactic.current", m.ship.CurrentSystem.Galaxy, m.ship.CurrentSystem.Index))
	fmt.Fprintln(&b)

	target := WayfarerSystemID{Galaxy: m.galacticTargetGalaxy, Index: m.galacticTargetIndex}
	fmt.Fprintln(&b, menuCursorStyle.Render(Tf("wf.galactic.target", target.Galaxy, target.Index)))
	fmt.Fprintln(&b)

	preview := GenerateSystem(target)
	fmt.Fprintln(&b, Tf("wf.galactic.preview", preview.Name, T(wayfarerGovernmentNameKeys[preview.Government]), preview.TechLevel))
	if m.ship.Visited[target] {
		fmt.Fprintln(&b, dimStyle.Render(T("wf.galactic.visited")))
	} else {
		fmt.Fprintln(&b, dimStyle.Render(T("wf.galactic.unvisited")))
	}

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, T("wf.galactic.hint"))
	m.renderMessage(&b)
	return b.String()
}

func (m WayfarerModel) viewTravel() string {
	var b strings.Builder
	fmt.Fprintln(&b, titleStyle.Render(T("wf.travel.title")))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, Tf("wf.travel.range", m.ship.FuelRange()))
	fmt.Fprintln(&b)

	if len(m.travelCandidates) == 0 {
		fmt.Fprintln(&b, dimStyle.Render(T("wf.travel.none_in_range")))
	}
	for i, id := range m.travelCandidates {
		sys := GenerateSystem(id)
		dist := m.currentSystem().Distance(sys)
		line := Tf("wf.travel.line", sys.Name, T(wayfarerGovernmentNameKeys[sys.Government]), dist)
		if i == m.cursor {
			line = menuCursorStyle.Render("> " + line)
		} else {
			line = "  " + line
		}
		fmt.Fprintln(&b, line)
	}
	galacticLine := T("wf.travel.galactic_jump")
	if m.cursor == len(m.travelCandidates) {
		galacticLine = menuCursorStyle.Render("> " + galacticLine)
	} else {
		galacticLine = "  " + galacticLine
	}
	fmt.Fprintln(&b, galacticLine)

	fmt.Fprintln(&b)
	fmt.Fprintln(&b, T("wf.travel.hint"))
	m.renderMessage(&b)
	return b.String()
}

func (m WayfarerModel) viewCombat() string {
	c := m.combat
	var b strings.Builder
	fmt.Fprintln(&b, titleStyle.Render(T("wf.combat.title")))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, Tf("wf.combat.enemy_line", T(c.EnemyNameKey), c.EnemyHull, c.EnemyMaxHull))
	fmt.Fprintln(&b, dimStyle.Render(Tf("wf.combat.enemy_weapon", T(wayfarerWeaponNameKey[c.EnemyWeapon]))))
	fmt.Fprintln(&b, Tf("wf.combat.enemy_shields_line", c.EnemyShieldFore, c.EnemyShieldAft, c.EnemyShieldLeft, c.EnemyShieldRight, c.EnemyMaxShield))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, Tf("wf.combat.player_hull", c.PlayerHull, c.PlayerMaxHull))
	fmt.Fprintln(&b, Tf("wf.combat.shields_line", c.PlayerShieldFore, c.PlayerShieldAft, c.PlayerShieldLeft, c.PlayerShieldRight, c.PlayerMaxShield))
	fmt.Fprintln(&b)
	if c.Round > 0 {
		fmt.Fprintln(&b, Tf("wf.combat.round_result", c.LastPlayerDamage, c.LastEnemyDamage, T(wayfarerFacingNameKey[c.LastHitFacing])))
	}
	if c.Outcome == CombatOngoing {
		fmt.Fprintln(&b, errorStyle.Render(Tf("wf.combat.approach_hint", T(wayfarerApproachNameKey[c.NextApproach]))))
	}

	if m.combatChoosingFacing && c.Outcome == CombatOngoing {
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, T("wf.combat.facing_choice.title"))
		fmt.Fprintf(&b, "[1] %s\n", T("wf.combat.facing.fore"))
		fmt.Fprintf(&b, "[2] %s\n", T("wf.combat.facing.aft"))
		fmt.Fprintf(&b, "[3] %s\n", T("wf.combat.facing.left"))
		fmt.Fprintf(&b, "[4] %s\n", T("wf.combat.facing.right"))
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, dimStyle.Render(T("wf.combat.facing_choice.hint")))
		m.renderMessage(&b)
		return b.String()
	}

	if m.combatDistributing && c.Outcome == CombatOngoing {
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, T("wf.combat.distribute.title"))
		focusLine := func(idx int, label string, val int) string {
			line := Tf("wf.combat.distribute.line", label, val)
			if idx == m.distFocus {
				return menuCursorStyle.Render("> " + line)
			}
			return "  " + line
		}
		fmt.Fprintln(&b, focusLine(0, T("wf.combat.facing.fore"), m.distFore))
		fmt.Fprintln(&b, focusLine(1, T("wf.combat.facing.aft"), m.distAft))
		fmt.Fprintln(&b, focusLine(2, T("wf.combat.facing.left"), m.distLeft))
		fmt.Fprintln(&b, focusLine(3, T("wf.combat.facing.right"), m.distRight))
		total := m.distFore + m.distAft + m.distLeft + m.distRight
		fmt.Fprintln(&b, Tf("wf.combat.distribute.total", total, c.PlayerMaxShield))
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, T("wf.combat.distribute.hint"))
		m.renderMessage(&b)
		return b.String()
	}

	switch c.Outcome {
	case CombatOngoing:
		fmt.Fprintln(&b)
		for i, a := range m.combatActions() {
			fmt.Fprintf(&b, "[%d] %s\n", i+1, a.Label)
		}
	case CombatWon:
		fmt.Fprintln(&b, winStyle.Render(T("wf.combat.outcome.won")))
	case CombatFled:
		fmt.Fprintln(&b, infoStyle.Render(T("wf.combat.outcome.fled")))
	case CombatNegotiated:
		fmt.Fprintln(&b, infoStyle.Render(T("wf.combat.outcome.negotiated")))
	case CombatLost:
		fmt.Fprintln(&b, errorStyle.Render(T("wf.combat.outcome.lost")))
	}
	m.renderMessage(&b)
	return b.String()
}

func (m WayfarerModel) viewQuestList() string {
	var b strings.Builder
	fmt.Fprintln(&b, titleStyle.Render(T("wf.quests.title")))
	fmt.Fprintln(&b)

	if m.ship.ActiveQuest != nil {
		def, _ := FindWayfarerQuest(m.ship.ActiveQuest.Key)
		fmt.Fprintln(&b, Tf("wf.quests.active", T(def.TitleKey)))
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, T("wf.quests.hint.continue"))
		m.renderMessage(&b)
		return b.String()
	}

	remaining := len(m.availableQuests())
	done := len(WayfarerQuestDefs) - remaining
	fmt.Fprintln(&b, dimStyle.Render(Tf("wf.quests.progress", done, len(WayfarerQuestDefs))))
	fmt.Fprintln(&b)
	if remaining == 0 {
		fmt.Fprintln(&b, T("wf.quests.all_done"))
	} else {
		fmt.Fprintln(&b, T("wf.quests.none_active"))
	}
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, T("wf.quests.hint.list"))
	m.renderMessage(&b)
	return b.String()
}

func (m WayfarerModel) viewQuestStep() string {
	var b strings.Builder
	def, _ := FindWayfarerQuest(m.ship.ActiveQuest.Key)
	step, ok := m.ship.CurrentQuestStep()

	fmt.Fprintln(&b, titleStyle.Render(T(def.TitleKey)))
	fmt.Fprintln(&b)

	if !ok {
		m.renderMessage(&b)
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, T("wf.quests.hint.back"))
		return b.String()
	}

	fmt.Fprintln(&b, wfWrap(T(step.TextKey), m.termWidth))
	fmt.Fprintln(&b)
	for i, c := range step.Choices {
		label := fmt.Sprintf("[%d] %s", i+1, T(c.LabelKey))
		fmt.Fprintln(&b, wfWrap(label, m.termWidth))
	}
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, T("wf.quests.hint.abandon"))
	m.renderMessage(&b)
	return b.String()
}

func (m WayfarerModel) viewGameOver() string {
	var b strings.Builder
	fmt.Fprintln(&b, titleStyle.Render(T("wf.title")))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, errorStyle.Render(T("wf.gameover.destroyed")))
	fmt.Fprintln(&b, Tf("wf.gameover.stats", T(m.ship.PilotRating()), m.ship.Kills))
	fmt.Fprintln(&b)
	fmt.Fprintln(&b, dimStyle.Render(T("wf.gameover.hint")))
	return b.String()
}

// wayfarerGovernmentNameKeys — ключи перевода названий правительств
// (индекс = значение WayfarerGovernment).
var wayfarerGovernmentNameKeys = [...]string{
	"wf.gov.anarchy", "wf.gov.feudal", "wf.gov.dictatorship", "wf.gov.communist",
	"wf.gov.confederacy", "wf.gov.democracy", "wf.gov.corporate",
}
