package main

import "time"

// ПРИМЕЧАНИЕ: инкрементальная (idle/clicker) игра в духе Candy Box 2
// — с постепенным открытием механик по мере накопления ресурса.
// Антураж — пародийная стилизация под сеттинг мрачного далёкого
// будущего (сервиторы, Муниторум, Скверна, зачистки), без
// копирования конкретных защищённых текстов, персонажей или
// фракций — только тематическая лексика.
//
// Прогресс существует только в рамках текущего запуска приложения
// (как и во всех остальных играх коллекции) — между перезапусками
// ничего не сохраняется.

// corpseStarchServitorDef — статическое описание одного тира
// генератора пассивного дохода. DemonicNameKey — альтернативное имя
// после перерождения в демоническую сущность (см. AcceptChaosOffer);
// сам механизм (стоимость, доход) не меняется — меняется только
// то, как это называется.
type corpseStarchServitorDef struct {
	NameKey        string
	DemonicNameKey string
	UnlockAt       float64 // порог по TotalEarned, при котором тир становится виден
	BaseCost       float64
	CostScale      float64
	YieldPerSec    float64
}

var corpseStarchServitorDefs = []corpseStarchServitorDef{
	{NameKey: "corpsestarch.servitor.skull", DemonicNameKey: "corpsestarch.servitor.pinkhorror", UnlockAt: 0, BaseCost: 10, CostScale: 1.15, YieldPerSec: 0.5},
	{NameKey: "corpsestarch.servitor.menial", DemonicNameKey: "corpsestarch.servitor.screamer", UnlockAt: 200, BaseCost: 100, CostScale: 1.15, YieldPerSec: 3},
	{NameKey: "corpsestarch.servitor.cogitator", DemonicNameKey: "corpsestarch.servitor.lordofchange", UnlockAt: 2000, BaseCost: 1000, CostScale: 1.15, YieldPerSec: 20},
}

// effectiveNameKey возвращает ключ имени тира с учётом текущего
// режима партии (обычный/демонический после перерождения).
func (def corpseStarchServitorDef) effectiveNameKey(mode CorpseStarchMode) string {
	if mode == CorpseStarchModeDemonic {
		return def.DemonicNameKey
	}
	return def.NameKey
}

// corpseStarchMissionDef — статическое описание одной доступной
// миссии зачистки (или, после перерождения, вторжения на планеты
// Империума — тот же механизм, другое название).
type corpseStarchMissionDef struct {
	NameKey        string
	DemonicNameKey string
	UnlockAt       float64
	Duration       time.Duration
	RewardMin      float64
	RewardMax      float64
}

var corpseStarchMissionDefs = []corpseStarchMissionDef{
	{NameKey: "corpsestarch.mission.vermin", DemonicNameKey: "corpsestarch.mission.borderworld", UnlockAt: 800, Duration: 20 * time.Second, RewardMin: 30, RewardMax: 60},
	{NameKey: "corpsestarch.mission.chapel", DemonicNameKey: "corpsestarch.mission.shrineworld", UnlockAt: 800, Duration: 60 * time.Second, RewardMin: 150, RewardMax: 250},
	{NameKey: "corpsestarch.mission.waaagh", DemonicNameKey: "corpsestarch.mission.sector", UnlockAt: 800, Duration: 150 * time.Second, RewardMin: 500, RewardMax: 800},
}

// effectiveNameKey возвращает ключ названия миссии с учётом текущего
// режима партии.
func (def corpseStarchMissionDef) effectiveNameKey(mode CorpseStarchMode) string {
	if mode == CorpseStarchModeDemonic {
		return def.DemonicNameKey
	}
	return def.NameKey
}

// Пороги открытия основных разделов интерфейса (по TotalEarned).
const (
	corpseStarchUnlockServitors = 20
	corpseStarchUnlockShop      = 80
	corpseStarchUnlockSump      = 300
	corpseStarchUnlockMissions  = 800
)

const (
	corpseStarchClickUpgradeBaseCost  = 25
	corpseStarchClickUpgradeCostScale = 1.6
	corpseStarchClickUpgradeGain      = 1
)

// Параметры варп-выброса: при достижении этого порога Скверна
// "прорывается" наружу — половина текущего запаса крахмала и
// половина каждого тира сервиторов теряется, Скверна сбрасывается в
// 0, и начинается вторжение хаоситов на фиксированное реальное
// время. Пока оно идёт, сервиторы дают заметно меньше дохода — часть
// сил станции отвлечена на оборону — а активная защита (повторные
// нажатия) даёт шанс на бонус по окончании вторжения.
const (
	corpseStarchCorruptionBreachThreshold = 3
	corpseStarchIncursionDuration         = 1 * time.Hour
	corpseStarchIncursionYieldMultiplier  = 0.5
	corpseStarchDefendClicksForBonus      = 10
	corpseStarchDefendBonusReward         = 200.0
)

// Параметры предложения перейти на сторону Хаоса и самого
// перерождения: после corpseStarchChaosMarksForOffer варп-выбросов
// подряд (без отказа, который сбрасывает счётчик) появляется выбор
// — отказаться (награда и сброс счётчика) или принять (переход в
// демонический режим после суток полной недееспособности).
const (
	corpseStarchChaosMarksForOffer = 5
	corpseStarchRebirthDuration    = 24 * time.Hour
	corpseStarchChaosDeclineReward = 500.0
)

// corpseStarchMission — активная (запущенная) миссия.
type corpseStarchMission struct {
	DefIdx    int
	StartedAt time.Time
}

// CorpseStarchGameState — полное состояние партии.
// CorpseStarchMode — обычный режим или демоническая сущность после
// перерождения (см. AcceptChaosOffer). Меняет только НАЗВАНИЯ вещей
// (ресурс, тиры сервиторов, миссии, счётчик Скверны/отблеска) — сам
// игровой механизм остаётся тем же самым.
type CorpseStarchMode int

const (
	CorpseStarchModeImperial CorpseStarchMode = iota
	CorpseStarchModeDemonic
)

// CorpseStarchGameState — полное состояние партии.
type CorpseStarchGameState struct {
	Starch      float64 // текущий (тратимый) запас
	TotalEarned float64 // накоплено за всю партию — используется для порогов открытия разделов

	ClickYield    float64
	ClickUpgrades int

	ServitorCounts []int // по одному счётчику на каждый элемент corpseStarchServitorDefs — уменьшается варп-выбросом

	// ServitorPurchases — сколько раз куплен каждый тир за всю
	// партию. В отличие от ServitorCounts, варп-выброс её НЕ трогает:
	// цена следующей покупки должна оставаться такой же, какой была
	// до потери инфраструктуры, а не дешеветь только потому, что
	// действующих юнитов вдруг стало меньше.
	ServitorPurchases []int

	Corruption int

	// ChaosIncursionUntil — если не нулевое время и ещё не наступило,
	// идёт вторжение хаоситов после варп-выброса (см. triggerWarpBreach).
	// ChaosDefendClicks считает, сколько раз игрок активно отбивался
	// за текущее вторжение — от этого зависит бонус по его окончании.
	ChaosIncursionUntil time.Time
	ChaosDefendClicks   int

	// ChaosMarks растёт на 1 после каждого варп-выброса. При
	// достижении corpseStarchChaosMarksForOffer появляется
	// предложение перейти на сторону Хаоса (см. ChaosOfferPending).
	ChaosMarks int

	// Mode — обычный режим или уже переродившаяся демоническая
	// сущность. RebirthUntil — если не нулевое и ещё не наступило,
	// идёт само перерождение: все обычные действия недоступны, пока
	// оно не завершится (см. InRebirth).
	Mode         CorpseStarchMode
	RebirthUntil time.Time

	ActiveMission *corpseStarchMission
	MissionsDone  int

	// BonusMultiplier / BonusUntil — временный множитель к клику от
	// удачного окунания цепного меча в отстойник.
	BonusMultiplier float64
	BonusUntil      time.Time

	LastTick time.Time

	Log []string // последние несколько строк лога событий, новые в конце
}

const corpseStarchLogLimit = 6

// NewCorpseStarchGame создаёт новую партию.
func NewCorpseStarchGame(now time.Time) *CorpseStarchGameState {
	return &CorpseStarchGameState{
		ClickYield:        1,
		BonusMultiplier:   1,
		ServitorCounts:    make([]int, len(corpseStarchServitorDefs)),
		ServitorPurchases: make([]int, len(corpseStarchServitorDefs)),
		LastTick:          now,
	}
}

func (g *CorpseStarchGameState) addLog(key string) {
	g.Log = append(g.Log, key)
	if len(g.Log) > corpseStarchLogLimit {
		g.Log = g.Log[len(g.Log)-corpseStarchLogLimit:]
	}
}

func (g *CorpseStarchGameState) earn(amount float64) {
	if amount <= 0 {
		return
	}
	g.Starch += amount
	g.TotalEarned += amount
}

// currentClickYield учитывает временный бонусный множитель, если он
// ещё действует на момент now.
func (g *CorpseStarchGameState) currentClickYield(now time.Time) float64 {
	if now.Before(g.BonusUntil) {
		return g.ClickYield * g.BonusMultiplier
	}
	return g.ClickYield
}

// Requisition — ручное действие ("нажать кнопку"): начисляет один
// клик дохода.
func (g *CorpseStarchGameState) Requisition(now time.Time) {
	g.earn(g.currentClickYield(now))
}

// Tick начисляет пассивный доход от сервиторов за время, прошедшее
// с последнего тика (now должен монотонно не убывать; для тестов
// можно подавать произвольные значения). Также сбрасывает истёкший
// временный бонус.
func (g *CorpseStarchGameState) Tick(now time.Time) {
	elapsed := now.Sub(g.LastTick).Seconds()
	g.LastTick = now
	g.checkRebirthCompletion(now)
	if elapsed <= 0 || g.InRebirth(now) {
		g.checkIncursionResolution(now)
		return
	}
	var perSec float64
	for i, def := range corpseStarchServitorDefs {
		perSec += float64(g.ServitorCounts[i]) * def.YieldPerSec
	}
	if g.InChaosIncursion(now) {
		perSec *= corpseStarchIncursionYieldMultiplier
	}
	g.earn(perSec * elapsed)
	g.checkIncursionResolution(now)
}

// checkRebirthCompletion переключает партию в демонический режим,
// как только суточное перерождение подходит к концу. Идемпотентно.
func (g *CorpseStarchGameState) checkRebirthCompletion(now time.Time) {
	if g.RebirthUntil.IsZero() || now.Before(g.RebirthUntil) {
		return
	}
	g.RebirthUntil = time.Time{}
	g.Mode = CorpseStarchModeDemonic
	g.addLog("corpsestarch.log.rebirth_complete")
}

// checkIncursionResolution проверяет, не истекло ли вторжение
// хаоситов, и если да — разрешает его (см. resolveChaosIncursion).
// Идемпотентно: повторные вызовы после разрешения ничего не делают,
// пока не начнётся новое вторжение.
func (g *CorpseStarchGameState) checkIncursionResolution(now time.Time) {
	if g.ChaosIncursionUntil.IsZero() || now.Before(g.ChaosIncursionUntil) {
		return
	}
	g.resolveChaosIncursion()
}

// servitorCost возвращает стоимость следующей покупки тира idx.
func (g *CorpseStarchGameState) servitorCost(idx int) float64 {
	def := corpseStarchServitorDefs[idx]
	cost := def.BaseCost
	for i := 0; i < g.ServitorPurchases[idx]; i++ {
		cost *= def.CostScale
	}
	return cost
}

// BuyServitor покупает одну единицу тира idx, если хватает
// трупного крахмала, тир уже открыт по порогу, и сейчас не идёт
// вторжение хаоситов (пока оно не закончится, новую инфраструктуру
// строить нельзя — часть станции физически недоступна под атакой).
func (g *CorpseStarchGameState) BuyServitor(idx int, now time.Time) bool {
	if idx < 0 || idx >= len(corpseStarchServitorDefs) {
		return false
	}
	if g.InChaosIncursion(now) {
		return false
	}
	if g.TotalEarned < corpseStarchServitorDefs[idx].UnlockAt {
		return false
	}
	cost := g.servitorCost(idx)
	if g.Starch < cost {
		return false
	}
	g.Starch -= cost
	g.ServitorCounts[idx]++
	g.ServitorPurchases[idx]++
	return true
}

func (g *CorpseStarchGameState) clickUpgradeCost() float64 {
	cost := float64(corpseStarchClickUpgradeBaseCost)
	for i := 0; i < g.ClickUpgrades; i++ {
		cost *= corpseStarchClickUpgradeCostScale
	}
	return cost
}

// BuyClickUpgrade повышает базовый доход от Реквизиции ("заточка
// цепного меча").
func (g *CorpseStarchGameState) BuyClickUpgrade() bool {
	cost := g.clickUpgradeCost()
	if g.Starch < cost {
		return false
	}
	g.Starch -= cost
	g.ClickUpgrades++
	g.ClickYield += corpseStarchClickUpgradeGain
	return true
}

// CorpseStarchDipOutcome — исход окунания цепного меча в отстойник.
type CorpseStarchDipOutcome int

const (
	DipBonusStarch CorpseStarchDipOutcome = iota
	DipMachineSpiritBlessing
	DipNothing
	DipCorruption
)

// DipChainsword бросает цепной меч в отстойник; roll — случайное
// число в [0,1), передаётся вызывающим кодом (реальный TUI передаёт
// rand.Float64(), тесты — фиксированные значения для детерминизма).
func (g *CorpseStarchGameState) DipChainsword(now time.Time, roll float64) CorpseStarchDipOutcome {
	switch {
	case roll < 0.5:
		bonus := 5 + roll*70 // немного разнообразия внутри диапазона [5,40)
		g.earn(bonus)
		g.addLog("corpsestarch.log.dip_bonus")
		return DipBonusStarch
	case roll < 0.75:
		g.BonusMultiplier = 2
		g.BonusUntil = now.Add(30 * time.Second)
		g.addLog("corpsestarch.log.dip_blessing")
		return DipMachineSpiritBlessing
	case roll < 0.9:
		g.addLog("corpsestarch.log.dip_nothing")
		return DipNothing
	default:
		g.Corruption++
		penalty := g.Starch * 0.1
		g.Starch -= penalty
		g.addLog("corpsestarch.log.dip_corruption")
		if g.Corruption >= corpseStarchCorruptionBreachThreshold {
			g.triggerWarpBreach(now)
		}
		return DipCorruption
	}
}

// triggerWarpBreach — Скверна прорывается варп-выбросом: половина
// текущего запаса крахмала и половина каждого тира сервиторов
// теряется, Скверна сбрасывается в 0 (выброс её "выпустил"), и
// начинается вторжение хаоситов на фиксированное реальное время.
func (g *CorpseStarchGameState) triggerWarpBreach(now time.Time) {
	g.Starch *= 0.5
	for i := range g.ServitorCounts {
		g.ServitorCounts[i] /= 2
	}
	g.Corruption = 0
	g.ChaosIncursionUntil = now.Add(corpseStarchIncursionDuration)
	g.ChaosDefendClicks = 0
	g.ChaosMarks++
	g.addLog("corpsestarch.log.warp_breach")
}

// ChaosOfferPending — пора ли показать предложение перейти на
// сторону Хаоса (набралось достаточно меток, партия ещё в обычном
// режиме и перерождение ещё не идёт).
func (g *CorpseStarchGameState) ChaosOfferPending() bool {
	return g.Mode == CorpseStarchModeImperial &&
		g.ChaosMarks >= corpseStarchChaosMarksForOffer &&
		g.RebirthUntil.IsZero()
}

// DeclineChaosOffer отклоняет предложение: счётчик меток сбрасывается
// в 0, взамен начисляется разовая награда.
func (g *CorpseStarchGameState) DeclineChaosOffer() {
	g.earn(corpseStarchChaosDeclineReward)
	g.ChaosMarks = 0
	g.addLog("corpsestarch.log.chaos_declined")
}

// AcceptChaosOffer принимает предложение: начинается перерождение —
// на corpseStarchRebirthDuration партия становится полностью
// недееспособной (см. InRebirth), а по его завершении Mode
// переключается на демонический (см. Tick).
func (g *CorpseStarchGameState) AcceptChaosOffer(now time.Time) {
	g.RebirthUntil = now.Add(corpseStarchRebirthDuration)
	g.ChaosMarks = 0
	g.addLog("corpsestarch.log.rebirth_begins")
}

// InRebirth — идёт ли прямо сейчас перерождение (после принятия
// предложения Хаоса, до его завершения).
func (g *CorpseStarchGameState) InRebirth(now time.Time) bool {
	return !g.RebirthUntil.IsZero() && now.Before(g.RebirthUntil)
}

// RebirthRemaining — сколько реального времени осталось до конца
// перерождения (0, если оно сейчас не идёт).
func (g *CorpseStarchGameState) RebirthRemaining(now time.Time) time.Duration {
	if !g.InRebirth(now) {
		return 0
	}
	return g.RebirthUntil.Sub(now)
}

// InChaosIncursion — идёт ли прямо сейчас вторжение хаоситов после
// варп-выброса.
func (g *CorpseStarchGameState) InChaosIncursion(now time.Time) bool {
	return !g.ChaosIncursionUntil.IsZero() && now.Before(g.ChaosIncursionUntil)
}

// ChaosIncursionRemaining — сколько реального времени осталось до
// конца вторжения (0, если вторжения сейчас нет).
func (g *CorpseStarchGameState) ChaosIncursionRemaining(now time.Time) time.Duration {
	if !g.InChaosIncursion(now) {
		return 0
	}
	return g.ChaosIncursionUntil.Sub(now)
}

// DefendAgainstChaos — активная защита от хаоситов (повторные
// нажатия во время вторжения). Возвращает false, если вторжения
// сейчас нет — действие недоступно вне его рамок.
func (g *CorpseStarchGameState) DefendAgainstChaos(now time.Time) bool {
	if !g.InChaosIncursion(now) {
		return false
	}
	g.ChaosDefendClicks++
	return true
}

// resolveChaosIncursion завершает текущее вторжение: если игрок
// набрал достаточно активных защит за время вторжения — начисляет
// бонус в благодарность за стойкость станции.
func (g *CorpseStarchGameState) resolveChaosIncursion() {
	if g.ChaosDefendClicks >= corpseStarchDefendClicksForBonus {
		g.earn(corpseStarchDefendBonusReward)
		g.addLog("corpsestarch.log.incursion_repelled_bonus")
	} else {
		g.addLog("corpsestarch.log.incursion_repelled")
	}
	g.ChaosIncursionUntil = time.Time{}
	g.ChaosDefendClicks = 0
}

// StartMission запускает миссию defIdx, если она открыта и сейчас
// нет другой активной миссии.
func (g *CorpseStarchGameState) StartMission(now time.Time, defIdx int) bool {
	if g.ActiveMission != nil {
		return false
	}
	if defIdx < 0 || defIdx >= len(corpseStarchMissionDefs) {
		return false
	}
	if g.TotalEarned < corpseStarchMissionDefs[defIdx].UnlockAt {
		return false
	}
	g.ActiveMission = &corpseStarchMission{DefIdx: defIdx, StartedAt: now}
	return true
}

// MissionRemaining возвращает оставшееся время активной миссии (0,
// если миссии нет или она уже завершена).
func (g *CorpseStarchGameState) MissionRemaining(now time.Time) time.Duration {
	if g.ActiveMission == nil {
		return 0
	}
	def := corpseStarchMissionDefs[g.ActiveMission.DefIdx]
	remaining := def.Duration - now.Sub(g.ActiveMission.StartedAt)
	if remaining < 0 {
		return 0
	}
	return remaining
}

// CollectMission забирает награду завершённой миссии (если она уже
// истекла); roll — случайное число в [0,1) для определения награды
// в диапазоне [RewardMin,RewardMax). Возвращает (награда, true) при
// успехе.
func (g *CorpseStarchGameState) CollectMission(now time.Time, roll float64) (float64, bool) {
	if g.ActiveMission == nil {
		return 0, false
	}
	if g.MissionRemaining(now) > 0 {
		return 0, false
	}
	def := corpseStarchMissionDefs[g.ActiveMission.DefIdx]
	reward := def.RewardMin + roll*(def.RewardMax-def.RewardMin)
	g.earn(reward)
	g.MissionsDone++
	g.ActiveMission = nil
	g.addLog("corpsestarch.log.mission_done")
	return reward, true
}

// ServitorsUnlocked, ShopUnlocked, SumpUnlocked, MissionsUnlocked —
// проверки открытия соответствующих разделов интерфейса.
func (g *CorpseStarchGameState) ServitorsUnlocked() bool {
	return g.TotalEarned >= corpseStarchUnlockServitors
}
func (g *CorpseStarchGameState) ShopUnlocked() bool {
	return g.TotalEarned >= corpseStarchUnlockShop
}
func (g *CorpseStarchGameState) SumpUnlocked() bool {
	return g.TotalEarned >= corpseStarchUnlockSump
}
func (g *CorpseStarchGameState) MissionsUnlocked() bool {
	return g.TotalEarned >= corpseStarchUnlockMissions
}

// ServitorUnlocked проверяет, виден ли конкретный тир idx (может
// быть ещё не открыт, даже если раздел "Сервиторы" уже виден).
func (g *CorpseStarchGameState) ServitorUnlocked(idx int) bool {
	return g.TotalEarned >= corpseStarchServitorDefs[idx].UnlockAt
}

// MissionUnlocked проверяет, видна ли конкретная миссия idx.
func (g *CorpseStarchGameState) MissionUnlocked(idx int) bool {
	return g.TotalEarned >= corpseStarchMissionDefs[idx].UnlockAt
}
