package main

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

	tea "github.com/charmbracelet/bubbletea"
)

// corpseStarchTickMsg — сигнал раз в секунду для начисления
// пассивного дохода в реальном времени, пока открыт экран игры.
type corpseStarchTickMsg struct{ At time.Time }

const corpseStarchTickInterval = 1 * time.Second

// corpseStarchAutosaveMsg — сигнал периодического автосохранения.
type corpseStarchAutosaveMsg struct{}

const corpseStarchAutosaveInterval = 15 * time.Second

// CorpseStarchModel — TUI поверх CorpseStarchGameState.
type CorpseStarchModel struct {
	game *CorpseStarchGameState

	message string

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

	termWidth int // для переноса длинного текста (например, предложения Хаоса) по ширине реального терминала

	quitting   bool
	backToMenu bool
}

// NewCorpseStarchModel загружает сохранённую партию, если она есть
// (начисляя честный пассивный доход за время отсутствия), либо
// создаёт новую.
func NewCorpseStarchModel() CorpseStarchModel {
	now := time.Now()
	g, err := LoadCorpseStarchGame()
	if err != nil {
		return CorpseStarchModel{game: NewCorpseStarchGame(now), termWidth: 80}
	}
	before := g.Starch
	g.Tick(now) // честно начисляем доход за время, пока партия была закрыта
	m := CorpseStarchModel{game: g, termWidth: 80}
	if earned := g.Starch - before; earned >= 1 {
		m.message = Tf("corpsestarch.msg.welcome_back", formatStarch(earned))
	}
	return m
}

func (m CorpseStarchModel) Init() tea.Cmd {
	return tea.Batch(corpseStarchScheduleTick(), corpseStarchScheduleAutosave())
}

func corpseStarchScheduleTick() tea.Cmd {
	return tea.Tick(corpseStarchTickInterval, func(t time.Time) tea.Msg { return corpseStarchTickMsg{At: t} })
}

func corpseStarchScheduleAutosave() tea.Cmd {
	return tea.Tick(corpseStarchAutosaveInterval, func(time.Time) tea.Msg { return corpseStarchAutosaveMsg{} })
}

func (m *CorpseStarchModel) setInfo(key string) {
	m.message = T(key)
}

// corpseStarchAction — один пункт динамического пронумерованного
// меню действий: что показать и что сделать по нажатию цифры.
type corpseStarchAction struct {
	Label  string
	Handle func(m *CorpseStarchModel, now time.Time)
}

// buildActions собирает список доступных прямо сейчас действий —
// от этого зависит, какая цифра что делает; список пересобирается
// на каждый рендер/обработку ввода, поэтому нумерация всегда
// соответствует актуально показанным пунктам.
func (m CorpseStarchModel) buildActions(now time.Time) []corpseStarchAction {
	g := m.game
	var actions []corpseStarchAction

	actions = append(actions, corpseStarchAction{
		Label: Tf("corpsestarch.action.requisition", formatStarch(g.currentClickYield(now))),
		Handle: func(m *CorpseStarchModel, now time.Time) {
			m.game.Requisition(now)
			m.setInfo("corpsestarch.msg.requisitioned")
		},
	})

	if g.ServitorsUnlocked() && !g.InChaosIncursion(now) {
		for i, def := range corpseStarchServitorDefs {
			if !g.ServitorUnlocked(i) {
				continue
			}
			i := i
			cost := g.servitorCost(i)
			actions = append(actions, corpseStarchAction{
				Label: Tf("corpsestarch.action.buy_servitor", T(def.effectiveNameKey(g.Mode)), g.ServitorCounts[i], formatStarch(cost)),
				Handle: func(m *CorpseStarchModel, now time.Time) {
					if m.game.BuyServitor(i, now) {
						m.setInfo("corpsestarch.msg.servitor_bought")
					} else {
						m.setInfo("corpsestarch.msg.cannot_afford")
					}
				},
			})
		}
	}

	if g.ShopUnlocked() {
		cost := g.clickUpgradeCost()
		actions = append(actions, corpseStarchAction{
			Label: Tf("corpsestarch.action.upgrade_click", formatStarch(cost)),
			Handle: func(m *CorpseStarchModel, now time.Time) {
				if m.game.BuyClickUpgrade() {
					m.setInfo("corpsestarch.msg.upgrade_bought")
				} else {
					m.setInfo("corpsestarch.msg.cannot_afford")
				}
			},
		})
	}

	if g.SumpUnlocked() && !g.InChaosIncursion(now) {
		actions = append(actions, corpseStarchAction{
			Label: T("corpsestarch.action.dip"),
			Handle: func(m *CorpseStarchModel, now time.Time) {
				outcome := m.game.DipChainsword(now, rand.Float64())
				switch outcome {
				case DipBonusStarch:
					m.setInfo("corpsestarch.log.dip_bonus")
				case DipMachineSpiritBlessing:
					m.setInfo("corpsestarch.log.dip_blessing")
				case DipNothing:
					m.setInfo("corpsestarch.log.dip_nothing")
				case DipCorruption:
					if m.game.InChaosIncursion(now) {
						// именно этот бросок пробил порог и запустил
						// вторжение прямо сейчас — раньше несколько
						// таких пробоев подряд могли перезапускать
						// вторжение, сбрасывая его таймер и прогресс
						// защиты; теперь пока идёт вторжение, само
						// действие окунания недоступно (см. условие
						// добавления этого действия выше), так что
						// сюда мы попадаем только один раз — в момент
						// самого пробоя
						m.setInfo("corpsestarch.log.warp_breach")
					} else {
						m.setInfo("corpsestarch.log.dip_corruption")
					}
				}
			},
		})
	}

	if g.InChaosIncursion(now) {
		defendKey := "corpsestarch.action.defend"
		if g.Mode == CorpseStarchModeDemonic {
			defendKey = "corpsestarch.action.defend.demonic"
		}
		actions = append(actions, corpseStarchAction{
			Label: Tf(defendKey, g.ChaosDefendClicks),
			Handle: func(m *CorpseStarchModel, now time.Time) {
				m.game.DefendAgainstChaos(now)
				m.setInfo("corpsestarch.msg.defended")
			},
		})
	}

	if g.MissionsUnlocked() {
		if g.ActiveMission == nil {
			for i, def := range corpseStarchMissionDefs {
				if !g.MissionUnlocked(i) {
					continue
				}
				i := i
				actions = append(actions, corpseStarchAction{
					Label: Tf("corpsestarch.action.start_mission", T(def.effectiveNameKey(g.Mode)), formatDuration(def.Duration)),
					Handle: func(m *CorpseStarchModel, now time.Time) {
						if m.game.StartMission(now, i) {
							m.setInfo("corpsestarch.msg.mission_started")
						}
					},
				})
			}
		} else if g.MissionRemaining(now) == 0 {
			actions = append(actions, corpseStarchAction{
				Label: T("corpsestarch.action.collect_mission"),
				Handle: func(m *CorpseStarchModel, now time.Time) {
					if _, ok := m.game.CollectMission(now, rand.Float64()); ok {
						m.setInfo("corpsestarch.log.mission_done")
					}
				},
			})
		}
	}

	return actions
}

func (m CorpseStarchModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.termWidth = msg.Width
		return m, nil
	case tea.KeyMsg:
		return m.handleKey(msg)
	case corpseStarchTickMsg:
		m.game.Tick(msg.At)
		return m, corpseStarchScheduleTick()
	case corpseStarchAutosaveMsg:
		_ = SaveCorpseStarchGame(m.game)
		return m, corpseStarchScheduleAutosave()
	}
	return m, nil
}

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

	now := time.Now()

	// Подтверждение "начать заново" проверяется в первую очередь —
	// это модальный вопрос, который может быть вызван из любого
	// экрана (включая перерождение), и должен работать одинаково
	// везде.
	if m.confirmingReset {
		switch key {
		case "y", "Y":
			_ = DeleteCorpseStarchSave()
			m.game = NewCorpseStarchGame(now)
			m.confirmingReset = false
			m.setInfo("corpsestarch.msg.reset_done")
		case "n", "N", "esc":
			m.confirmingReset = false
			m.message = ""
		}
		return m, nil
	}

	// Во время перерождения в самой игре нельзя ничего сделать, но
	// сохраниться и выйти (q), либо начать всё заново (N) — можно,
	// как и в любой другой момент партии.
	if m.game.InRebirth(now) {
		switch key {
		case "q":
			_ = SaveCorpseStarchGame(m.game)
			m.backToMenu = true
		case "N":
			m.confirmingReset = true
		}
		return m, nil
	}

	// Предложение перейти на сторону Хаоса — блокирует обычные
	// действия, пока не будет дан явный ответ (или можно сохраниться
	// и обдумать это позже — предложение никуда не денется).
	if m.game.ChaosOfferPending() {
		switch key {
		case "y", "Y":
			m.game.AcceptChaosOffer(now)
		case "n", "N":
			m.game.DeclineChaosOffer()
		case "q":
			_ = SaveCorpseStarchGame(m.game)
			m.backToMenu = true
		}
		return m, nil
	}

	if key == "q" {
		_ = SaveCorpseStarchGame(m.game)
		m.backToMenu = true
		return m, nil
	}
	if key == "N" {
		m.confirmingReset = true
		return m, nil
	}

	actions := m.buildActions(now)
	for i := 0; i < len(actions) && i < 9; i++ {
		if key == fmt.Sprintf("%d", i+1) {
			actions[i].Handle(&m, now)
			return m, nil
		}
	}
	return m, nil
}

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

// formatStarch форматирует значение ресурса компактно (без длинного
// хвоста дробной части).
func formatStarch(v float64) string {
	if v == float64(int64(v)) {
		return fmt.Sprintf("%d", int64(v))
	}
	return fmt.Sprintf("%.1f", v)
}

func formatDuration(d time.Duration) string {
	d = d.Round(time.Second)
	h := int(d.Hours())
	m := int(d.Minutes()) % 60
	s := int(d.Seconds()) % 60
	switch {
	case h > 0:
		return fmt.Sprintf("%dh%02dm%02ds", h, m, s)
	case m > 0:
		return fmt.Sprintf("%dm%02ds", m, s)
	default:
		return fmt.Sprintf("%ds", s)
	}
}

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

	g := m.game
	now := time.Now()
	var b strings.Builder

	fmt.Fprintln(&b, titleStyle.Render(T("corpsestarch.title")))
	fmt.Fprintln(&b)

	if m.confirmingReset {
		fmt.Fprintln(&b, errorStyle.Render(T("corpsestarch.confirm_reset")))
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, dimStyle.Render(T("corpsestarch.confirm_reset.hint")))
		return b.String()
	}

	if g.InRebirth(now) {
		fmt.Fprintln(&b, errorStyle.Render(Tf("corpsestarch.rebirth.active", formatDuration(g.RebirthRemaining(now)))))
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, dimStyle.Render(T("corpsestarch.rebirth.hint")))
		return b.String()
	}

	if g.ChaosOfferPending() {
		fmt.Fprintln(&b, errorStyle.Render(T("corpsestarch.chaos.offer.title")))
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, wrapText(T("corpsestarch.chaos.offer.text"), m.termWidth))
		fmt.Fprintln(&b)
		fmt.Fprintln(&b, T("corpsestarch.chaos.offer.accept"))
		fmt.Fprintln(&b, T("corpsestarch.chaos.offer.decline"))
		return b.String()
	}

	if g.Mode == CorpseStarchModeDemonic {
		fmt.Fprintln(&b, Tf("corpsestarch.header.starch.demonic", formatStarch(g.Starch), formatStarch(g.TotalEarned)))
	} else {
		fmt.Fprintln(&b, Tf("corpsestarch.header.starch", formatStarch(g.Starch), formatStarch(g.TotalEarned)))
	}
	if g.SumpUnlocked() {
		if g.Mode == CorpseStarchModeDemonic {
			fmt.Fprintln(&b, Tf("corpsestarch.header.astronomican", g.Corruption))
		} else {
			fmt.Fprintln(&b, Tf("corpsestarch.header.corruption", g.Corruption))
		}
	}
	fmt.Fprintln(&b)

	if g.InChaosIncursion(now) {
		bannerKey := "corpsestarch.incursion.active"
		if g.Mode == CorpseStarchModeDemonic {
			bannerKey = "corpsestarch.incursion.active.demonic"
		}
		fmt.Fprintln(&b, errorStyle.Render(Tf(bannerKey, formatDuration(g.ChaosIncursionRemaining(now)))))
		fmt.Fprintln(&b)
	}

	if g.MissionsUnlocked() && g.ActiveMission != nil {
		def := corpseStarchMissionDefs[g.ActiveMission.DefIdx]
		remaining := g.MissionRemaining(now)
		if remaining > 0 {
			fmt.Fprintln(&b, Tf("corpsestarch.mission.inprogress", T(def.effectiveNameKey(g.Mode)), formatDuration(remaining)))
		} else {
			fmt.Fprintln(&b, winStyle.Render(Tf("corpsestarch.mission.ready", T(def.effectiveNameKey(g.Mode)))))
		}
		fmt.Fprintln(&b)
	}

	actions := m.buildActions(now)
	for i, a := range actions {
		if i >= 9 {
			break
		}
		fmt.Fprintf(&b, "[%d] %s\n", i+1, a.Label)
	}
	fmt.Fprintln(&b)

	if len(g.Log) > 0 {
		fmt.Fprintln(&b, dimStyle.Render(T("corpsestarch.log.title")))
		for _, key := range g.Log {
			fmt.Fprintln(&b, dimStyle.Render("  "+wrapText(T(key), m.termWidth)))
		}
		fmt.Fprintln(&b)
	}

	if m.message != "" {
		fmt.Fprintln(&b, infoStyle.Render(wrapText(m.message, m.termWidth)))
		fmt.Fprintln(&b)
	}

	fmt.Fprintln(&b, dimStyle.Render(T("corpsestarch.hint")))
	return b.String()
}
