# retoor <retoor@molodetz.nl>

from __future__ import annotations

import hashlib
import math
from dataclasses import dataclass

STARTING_COINS = 50
STARTING_PLOTS = 4
MAX_PLOTS = 12
MAX_LEVEL = 20

PLOT_BASE_COST = 100
WATER_BONUS_PCT = 0.08
MAX_WATERS_PER_PLOT = 3
WATER_REWARD_COINS = 6
WATER_REWARD_XP = 3

STEAL_GRACE_SECONDS = 60
STEAL_FRACTION = 0.5
STEAL_COOLDOWN_SECONDS = 3600
STEAL_MAX_PER_VICTIM_PER_DAY = 3

GOLDEN_CHANCE = 0.05
GOLDEN_MULTIPLIER = 5

GAME_SITE_XP_DIVISOR = 25


@dataclass(frozen=True)
class Crop:
    key: str
    name: str
    icon: str
    cost: int
    grow_seconds: int
    reward_coins: int
    reward_xp: int
    min_level: int
    min_mastery: int = 0
    steal_immune: bool = False
    era_key: str | None = None


CROPS: tuple[Crop, ...] = (
    Crop("shell", "Shell Script", "\U0001f41a", 5, 30, 11, 2, 1),
    Crop("python", "Python Script", "\U0001f40d", 15, 120, 36, 5, 1),
    Crop("webapp", "Web App", "\U0001f4dc", 40, 300, 98, 12, 2),
    Crop("api", "Go Service", "\U0001f439", 90, 600, 224, 25, 3),
    Crop("rust", "Rust Engine", "\U0001f980", 200, 1800, 520, 60, 4),
    Crop("haskell", "Compiler", "λ", 500, 3600, 1380, 150, 6),
    Crop("kernel", "Kernel", "⚙️", 1200, 7200, 3600, 400, 8),
    Crop("distsys", "Distributed System", "\U0001f578", 5000, 14400, 11300, 900, MAX_LEVEL, min_mastery=1),
    Crop("mlpipe", "ML Pipeline", "\U0001f9e0", 12000, 21600, 21000, 1800, MAX_LEVEL, min_mastery=1),
    Crop("secfort", "Security Fortress", "\U0001f510", 30000, 28800, 48000, 3200, MAX_LEVEL, min_mastery=1, steal_immune=True),
)

CROP_BY_KEY = {crop.key: crop for crop in CROPS}
REGISTRY_BOOST_CROPS = ("rust", "haskell", "kernel")


def site_xp_for(game_xp: int) -> int:
    return max(0, int(game_xp) // GAME_SITE_XP_DIVISOR)


@dataclass(frozen=True)
class CiTier:
    tier: int
    label: str
    speed: float
    upgrade_cost: int


CI_TIERS: tuple[CiTier, ...] = (
    CiTier(1, "Local Build", 1.0, 0),
    CiTier(2, "Shared Runner", 1.25, 150),
    CiTier(3, "Fast Runner", 1.6, 400),
    CiTier(4, "Parallel Matrix", 2.0, 1000),
    CiTier(5, "Distributed Cache", 2.5, 2600),
)

CI_BY_TIER = {tier.tier: tier for tier in CI_TIERS}
MAX_CI_TIER = CI_TIERS[-1].tier


def crop_for(key: str) -> Crop | None:
    return CROP_BY_KEY.get(key)


def ci_speed(tier: int) -> float:
    entry = CI_BY_TIER.get(tier)
    return entry.speed if entry else 1.0


def next_ci_tier(tier: int) -> CiTier | None:
    return CI_BY_TIER.get(tier + 1)


REGISTRY_BOOST_FACTOR = 1.15


def farm_speed(ci_tier: int, growth_level: int = 0, legacy_speed_level: int = 0) -> float:
    return (
        ci_speed(ci_tier)
        * (1 + PERK_BY_KEY["growth"].step * growth_level)
        * (1 + LEGACY_SPEED_STEP * legacy_speed_level)
    )


def grow_seconds_for(
    crop: Crop,
    ci_tier: int,
    growth_level: int = 0,
    legacy_speed_level: int = 0,
    registry_boost: bool = False,
) -> int:
    speed = farm_speed(ci_tier, growth_level, legacy_speed_level)
    if registry_boost and crop.key in REGISTRY_BOOST_CROPS:
        speed *= REGISTRY_BOOST_FACTOR
    return max(1, round(crop.grow_seconds / speed))


def water_bonus_seconds(
    crop: Crop, ci_tier: int, growth_level: int = 0, legacy_speed_level: int = 0
) -> int:
    return max(
        1,
        round(
            grow_seconds_for(crop, ci_tier, growth_level, legacy_speed_level)
            * WATER_BONUS_PCT
        ),
    )


def plot_cost(current_plot_count: int) -> int:
    extra = current_plot_count - STARTING_PLOTS
    return PLOT_BASE_COST * (2 ** max(0, extra))


def xp_threshold(level: int) -> int:
    return 50 * (level - 1) * (level - 1)


def level_for_xp(xp: int) -> int:
    level = 1
    while level < MAX_LEVEL and xp >= xp_threshold(level + 1):
        level += 1
    return level


def level_progress(xp: int) -> dict:
    level = level_for_xp(xp)
    floor_xp = xp_threshold(level)
    if level >= MAX_LEVEL:
        return {
            "level": level,
            "xp": xp,
            "into_level": xp - floor_xp,
            "span": 0,
            "next_level_xp": floor_xp,
            "is_max": True,
        }
    ceil_xp = xp_threshold(level + 1)
    return {
        "level": level,
        "xp": xp,
        "into_level": xp - floor_xp,
        "span": ceil_xp - floor_xp,
        "next_level_xp": ceil_xp,
        "is_max": False,
    }


SCORE_PRESTIGE = 5000
SCORE_HARVEST = 10
SCORE_COIN_DIVISOR = 20
SCORE_CI = 250
SCORE_PLOT = 200
SCORE_PERK = 120
SCORE_STREAK = 15
SCORE_STREAK_CAP = 30
SCORE_COIN_CAP = 100_000


def farm_score(farm: dict) -> int:
    def value(key: str, default: int = 0) -> int:
        raw = farm.get(key)
        return int(raw) if raw is not None else default

    perk_total = sum(value(f"perk_{perk.key}") for perk in PERKS)
    return (
        value("xp")
        + value("prestige") * SCORE_PRESTIGE
        + value("total_harvests") * SCORE_HARVEST
        + min(value("coins") // SCORE_COIN_DIVISOR, SCORE_COIN_CAP)
        + (value("ci_tier", 1) - 1) * SCORE_CI
        + (value("plot_count", STARTING_PLOTS) - STARTING_PLOTS) * SCORE_PLOT
        + perk_total * SCORE_PERK
        + min(value("streak"), SCORE_STREAK_CAP) * SCORE_STREAK
    )


def unlocked_crops(
    level: int, mastery_earned: int = 0, active_era_name: str | None = None
) -> list[Crop]:
    return [
        crop
        for crop in CROPS
        if crop.min_level <= level
        and crop.min_mastery <= mastery_earned
        and (crop.era_key is None or crop.era_key == active_era_name)
    ]


def crop_lock_reason(
    crop: Crop, level: int, mastery_earned: int, era_locked: bool = False
) -> tuple[str, str] | None:
    if crop.min_level > level:
        return "level", f"unlocks at level {crop.min_level}"
    if crop.min_mastery > mastery_earned:
        return "mastery", "unlocks after reaching Mastery (Refactor to prestige 50)"
    if era_locked:
        return "era", "is not available right now"
    return None


def crop_payload(
    crop: Crop,
    ci_tier: int,
    level: int,
    growth_level: int = 0,
    discount_level: int = 0,
    yield_level: int = 0,
    prestige: int = 0,
    legacy_mult_level: int = 0,
    legacy_speed_level: int = 0,
    market_factor: float = 1.0,
    mastery_earned: int = 0,
    active_era_name: str | None = None,
    registry_boost: bool = False,
) -> dict:
    era_locked = crop.era_key is not None and crop.era_key != active_era_name
    market_state = "normal"
    if market_factor < 1.0:
        market_state = "saturated"
    elif market_factor > 1.0:
        market_state = "boosted"
    lock = crop_lock_reason(crop, level, mastery_earned, era_locked)
    return {
        "key": crop.key,
        "name": crop.name,
        "icon": crop.icon,
        "cost": effective_plant_cost(crop, discount_level),
        "reward_coins": effective_reward_coins(
            crop, yield_level, prestige, legacy_mult_level, market_factor
        ),
        "reward_xp": crop.reward_xp,
        "min_level": crop.min_level,
        "min_mastery": crop.min_mastery,
        "grow_seconds": grow_seconds_for(
            crop, ci_tier, growth_level, legacy_speed_level, registry_boost
        ),
        "locked": lock is not None,
        "locked_reason": lock[0] if lock else "",
        "locked_text": f"{crop.name} {lock[1]}." if lock else "",
        "market_state": market_state,
    }


@dataclass(frozen=True)
class Perk:
    key: str
    name: str
    icon: str
    description: str
    max_level: int
    base_cost: int
    cost_growth: float
    step: float


PERKS: tuple[Perk, ...] = (
    Perk("yield", "Optimizer", "📈", "+5% harvest coins per level", 10, 120, 1.6, 0.05),
    Perk("growth", "Build Cache", "⚡", "+4% build speed per level", 10, 150, 1.7, 0.04),
    Perk("discount", "Bulk Licenses", "🏷️", "-3% planting cost per level", 8, 100, 1.7, 0.03),
    Perk("xp", "Mentorship", "🎓", "+5% harvest XP per level", 10, 140, 1.6, 0.05),
)

PERK_BY_KEY = {perk.key: perk for perk in PERKS}

PRESTIGE_MIN_LEVEL = 10
PRESTIGE_BONUS = 0.25

REFACTOR_BASE_COST = 20_000
REFACTOR_WEALTH_PCT = 0.15
REFACTOR_CARRYOVER_BASE = 0.10
REFACTOR_CARRYOVER_MAX = 0.95
LEGACY_CARRYOVER_STEP = 0.05

STAR_BASE = 1
LEGACY_SPEED_STEP = 0.05
LEGACY_MULT_STEP = 0.10
LEGACY_DEFENSE_GRACE = 30
LEGACY_DEFENSE_FRACTION = 0.05


@dataclass(frozen=True)
class LegacyUpgrade:
    key: str
    name: str
    icon: str
    description: str
    max_level: int
    base_cost: int
    cost_growth: float


LEGACY_UPGRADES: tuple[LegacyUpgrade, ...] = (
    LegacyUpgrade(
        "autoharvest",
        "CI Bot",
        "🤖",
        "Auto-collect ready builds when you open your farm",
        1,
        3,
        1.0,
    ),
    LegacyUpgrade(
        "multiplier",
        "Tech Debt Payoff",
        "💎",
        "+10% coins per level, stacks with prestige",
        10,
        1,
        1.6,
    ),
    LegacyUpgrade(
        "speed", "Bare-Metal", "🏎️", "+5% base build speed per level", 8, 1, 1.7
    ),
    LegacyUpgrade(
        "plots",
        "Monorepo",
        "🗂️",
        "+1 starting plot after refactor per level",
        MAX_PLOTS - STARTING_PLOTS,
        3,
        2.0,
    ),
    LegacyUpgrade(
        "defense",
        "Branch Protection",
        "🛡️",
        "+30s steal grace and -5% steal loss per level",
        5,
        2,
        1.8,
    ),
    LegacyUpgrade(
        "carryover",
        "Golden Parachute",
        "🪂",
        "+5% of your post-fee coins carried through each refactor per level",
        10,
        2,
        1.8,
    ),
)

LEGACY_BY_KEY = {up.key: up for up in LEGACY_UPGRADES}


def legacy_for(key: str) -> LegacyUpgrade | None:
    return LEGACY_BY_KEY.get(key)


def legacy_cost(up: LegacyUpgrade, level: int) -> int:
    return round(up.base_cost * (up.cost_growth ** level))


def legacy_multiplier(level: int) -> float:
    return 1 + LEGACY_MULT_STEP * max(0, level)


def stars_for_refactor(level: int, prestige: int) -> int:
    return STAR_BASE + level // 5 + max(0, prestige)


def effective_steal_grace(defense_level: int = 0, extra_seconds: int = 0) -> int:
    return STEAL_GRACE_SECONDS + LEGACY_DEFENSE_GRACE * max(0, defense_level) + max(0, extra_seconds)


def effective_steal_fraction(
    defense_level: int = 0,
    floor: float = 0.1,
    building_reduction: float = 0.0,
    cap: float = 1.0,
) -> float:
    reduction = min(1.0, max(0.0, building_reduction))
    base = (STEAL_FRACTION - LEGACY_DEFENSE_FRACTION * max(0, defense_level)) * (1 - reduction)
    return min(cap, max(floor, base))


def prestige_base_plots(legacy_plots_level: int = 0) -> int:
    return STARTING_PLOTS + max(0, legacy_plots_level)


def legacy_value_text(up: LegacyUpgrade, level: int) -> str:
    if up.key == "autoharvest":
        return "Active" if level > 0 else "Inactive"
    if up.key == "multiplier":
        return f"+{round(LEGACY_MULT_STEP * level * 100)}% coins"
    if up.key == "speed":
        return f"+{round(LEGACY_SPEED_STEP * level * 100)}% build speed"
    if up.key == "plots":
        return f"+{level} starting plots"
    if up.key == "carryover":
        pct = round(refactor_carryover_fraction(level) * 100)
        return f"{pct}% refactor carry-over"
    grace = LEGACY_DEFENSE_GRACE * level
    loss = round(LEGACY_DEFENSE_FRACTION * level * 100)
    return f"+{grace}s grace, -{loss}% steal loss"


def is_golden(plot_uid: str, planted_at: str) -> bool:
    if not plot_uid or not planted_at:
        return False
    digest = hashlib.sha256(f"{plot_uid}:{planted_at}".encode()).hexdigest()
    return (int(digest, 16) % 1000) < round(GOLDEN_CHANCE * 1000)


DAILY_BASE = 20
DAILY_STREAK_STEP = 12
DAILY_STREAK_CAP = 7

FERTILIZE_FRACTION = 0.5
FERTILIZE_TAX = 1.05


def perk_for(key: str) -> Perk | None:
    return PERK_BY_KEY.get(key)


def perk_cost(perk: Perk, current_level: int) -> int:
    return round(perk.base_cost * (perk.cost_growth ** current_level))


def perk_value_text(perk: Perk, level: int) -> str:
    if perk.key == "discount":
        return f"-{round(perk.step * level * 100)}% planting cost"
    if perk.key == "growth":
        return f"+{round(perk.step * level * 100)}% build speed"
    if perk.key == "yield":
        return f"+{round(perk.step * level * 100)}% harvest coins"
    return f"+{round(perk.step * level * 100)}% harvest XP"


def prestige_multiplier(prestige: int) -> float:
    return 1 + PRESTIGE_BONUS * max(0, prestige)


def refactor_cost(prestige: int, coins: int) -> int:
    scaled = REFACTOR_BASE_COST * (1 + max(0, prestige)) * prestige_multiplier(prestige)
    return round(scaled + REFACTOR_WEALTH_PCT * max(0, coins))


def refactor_carryover_fraction(carryover_level: int = 0) -> float:
    return min(
        REFACTOR_CARRYOVER_MAX,
        REFACTOR_CARRYOVER_BASE + LEGACY_CARRYOVER_STEP * max(0, carryover_level),
    )


def refactor_carryover(coins: int, fee: int, carryover_level: int = 0) -> int:
    remainder = max(0, coins - fee)
    return int(remainder * refactor_carryover_fraction(carryover_level))


GRANT_WEALTH_CEILING = 10_000
GRANT_MIN_WEEK_HARVESTS = 5
GRANT_MAX_PRESTIGE = 5
GRANT_CAP = 2_500
GRANT_MIN_AMOUNT = 250


def grant_amount(treasury_balance: int, eligible_farms: int = 1) -> int:
    share = max(0, treasury_balance) // max(1, eligible_farms)
    amount = min(GRANT_CAP, share)
    return amount if amount >= GRANT_MIN_AMOUNT else 0


UNDERDOG_COIN_RATIO = 10
UNDERDOG_DURATION_HOURS = 24
UNDERDOG_MULTIPLIER = 1.25


def effective_plant_cost(crop: Crop, discount_level: int = 0) -> int:
    factor = max(0.0, 1 - PERK_BY_KEY["discount"].step * discount_level)
    return max(1, round(crop.cost * factor))


def effective_reward_coins(
    crop: Crop,
    yield_level: int = 0,
    prestige: int = 0,
    legacy_mult_level: int = 0,
    market_factor: float = 1.0,
    underdog: bool = False,
) -> int:
    factor = (
        (1 + PERK_BY_KEY["yield"].step * yield_level)
        * prestige_multiplier(prestige)
        * legacy_multiplier(legacy_mult_level)
        * market_factor
    )
    if underdog:
        factor *= UNDERDOG_MULTIPLIER
    return round(crop.reward_coins * factor)


def effective_reward_xp(crop: Crop, xp_level: int = 0) -> int:
    return round(crop.reward_xp * (1 + PERK_BY_KEY["xp"].step * xp_level))


def steal_reward_coins(
    crop: Crop,
    yield_level: int = 0,
    prestige: int = 0,
    legacy_mult_level: int = 0,
    defense_level: int = 0,
    market_factor: float = 1.0,
    steal_floor: float = 0.1,
    steal_reduction: float = 0.0,
    steal_cap: float = 1.0,
) -> int:
    fraction = effective_steal_fraction(defense_level, steal_floor, steal_reduction, steal_cap)
    return max(
        1,
        round(
            effective_reward_coins(crop, yield_level, prestige, legacy_mult_level, market_factor)
            * fraction
        ),
    )


def social_reward_scale(prestige: int = 0, legacy_mult_level: int = 0) -> float:
    return prestige_multiplier(prestige) * legacy_multiplier(legacy_mult_level)


def water_reward_coins(prestige: int = 0, legacy_mult_level: int = 0) -> int:
    scale = social_reward_scale(prestige, legacy_mult_level)
    return max(WATER_REWARD_COINS, round(WATER_REWARD_COINS * scale))


def daily_reward(streak: int, income_scale: float = 1.0) -> int:
    effective = min(max(streak, 1), DAILY_STREAK_CAP)
    base = DAILY_BASE + DAILY_STREAK_STEP * (effective - 1)
    return max(base, round(base * max(1.0, income_scale)))


def realizable_harvest_coins(
    crop: Crop,
    yield_level: int = 0,
    prestige: int = 0,
    legacy_mult_level: int = 0,
    market_factor: float = 1.0,
    golden: bool = False,
    contract_boost: bool = False,
    underdog: bool = False,
    canary: bool = False,
) -> int:
    coins = effective_reward_coins(
        crop, yield_level, prestige, legacy_mult_level, market_factor, underdog
    )
    if golden:
        coins *= GOLDEN_MULTIPLIER
    if contract_boost:
        coins = round(coins * WEEKLY_CONTRACT_BOOST_MULTIPLIER)
    if canary:
        coins = round(coins * (1 + CANARY_DOUBLE_CHANCE))
    return coins


def fertilize_click_cost(
    realizable_coins: int, reduce_seconds: int, full_grow_seconds: int
) -> int:
    if reduce_seconds < 1 or full_grow_seconds < 1:
        return 0
    return max(
        1,
        math.ceil(realizable_coins * reduce_seconds / full_grow_seconds * FERTILIZE_TAX),
    )


@dataclass(frozen=True)
class QuestDef:
    kind: str
    label: str
    goal_min: int
    goal_max: int
    coin: int
    xp: int
    contract_stars: int = 3
    contract_xp: int = 40


QUEST_DEFS: dict[str, QuestDef] = {
    "plant": QuestDef("plant", "Plant {goal} crops", 3, 6, 8, 2, 3, 40),
    "harvest": QuestDef("harvest", "Harvest {goal} builds", 3, 5, 14, 4, 3, 40),
    "water": QuestDef("water", "Water {goal} neighbour builds", 2, 4, 16, 5, 3, 30),
    "earn": QuestDef("earn", "Earn {goal} coins harvesting", 150, 400, 0, 0, 5, 60),
}

QUEST_KINDS = tuple(QUEST_DEFS.keys())
DAILY_QUEST_COUNT = 3


def daily_quests(user_uid: str, day: str, income_scale: float = 1.0) -> list[dict]:
    seed = int(hashlib.sha256(f"{user_uid}:{day}".encode()).hexdigest(), 16)
    start = seed % len(QUEST_KINDS)
    scale = max(1.0, income_scale)
    quests = []
    for index in range(DAILY_QUEST_COUNT):
        kind = QUEST_KINDS[(start + index) % len(QUEST_KINDS)]
        definition = QUEST_DEFS[kind]
        span = definition.goal_max - definition.goal_min + 1
        goal = definition.goal_min + ((seed >> (index * 7)) % span)
        if kind == "earn":
            goal = max(definition.goal_min, int(goal * scale // 10) * 10)
            reward_coins = round(goal * 0.15)
            reward_xp = max(1, round(goal / 30 / scale))
        else:
            reward_coins = round(goal * definition.coin * scale)
            reward_xp = goal * definition.xp
        quests.append(
            {
                "kind": kind,
                "label": definition.label.format(goal=goal),
                "goal": goal,
                "reward_coins": reward_coins,
                "reward_xp": reward_xp,
            }
        )
    return quests


WEEKLY_CONTRACT_BOOST_MULTIPLIER = 1.2
WEEKLY_CONTRACT_BOOST_HOURS = 48


def weekly_contract(user_uid: str, iso_week: str, income_scale: float = 1.0) -> dict:
    seed = int(hashlib.sha256(f"{user_uid}:{iso_week}".encode()).hexdigest(), 16)
    kind = QUEST_KINDS[seed % len(QUEST_KINDS)]
    definition = QUEST_DEFS[kind]
    goal = (definition.goal_max + definition.goal_min) * 4
    if kind == "earn":
        scaled = goal * max(1.0, income_scale)
        goal = max(definition.goal_min, int(scaled // 10) * 10)
    return {
        "kind": kind,
        "label": f"Weekly: {definition.label.format(goal=goal)}",
        "goal": goal,
        "reward_stars": definition.contract_stars,
        "reward_xp": definition.contract_xp,
    }


MARKET_WINDOW_HOURS = 48
MARKET_SATURATION_TIERS: tuple[tuple[float, float], ...] = (
    (0.0, 1.00),
    (8.0, 0.85),
    (24.0, 0.70),
    (48.0, 0.55),
    (96.0, 0.40),
)
MARKET_BUFFED_CROPS = ("shell", "python", "webapp", "api")
MARKET_PRESSURE_CROPS = ("rust", "haskell", "kernel")
MARKET_BUFF_CAP = 1.15


def supply_days(crop: Crop, recent_harvests: int, active_farms: int = 1) -> float:
    return recent_harvests * crop.grow_seconds / 86400.0 / max(1, active_farms)


def market_saturation_factor(supply: float) -> float:
    factor = MARKET_SATURATION_TIERS[0][1]
    for threshold, tier_factor in MARKET_SATURATION_TIERS:
        if supply >= threshold:
            factor = tier_factor
    return factor


def market_buff_factor(crop_key: str, pressure: float) -> float:
    if crop_key not in MARKET_BUFFED_CROPS:
        return 1.0
    max_pressure = 1.0 - MARKET_SATURATION_TIERS[-1][1]
    scale = (MARKET_BUFF_CAP - 1.0) / max_pressure
    return min(MARKET_BUFF_CAP, 1.0 + pressure * scale)


@dataclass(frozen=True)
class Infrastructure:
    key: str
    name: str
    icon: str
    description: str
    cost: int
    min_prestige: int


INFRASTRUCTURE: tuple[Infrastructure, ...] = (
    Infrastructure(
        "registry",
        "Private Registry",
        "\U0001f4e6",
        "Rust, Compiler, and Kernel crops grow 15% faster",
        3_000_000,
        3,
    ),
    Infrastructure(
        "canary",
        "Canary Deployments",
        "\U0001f424",
        "Every harvest has a 12% chance to double and a 6% chance to only refund its planting cost",
        6_000_000,
        8,
    ),
    Infrastructure(
        "observability",
        "Observability Suite",
        "\U0001f52d",
        "Caps what a raider can take from you at 20% of a build's value",
        15_000_000,
        15,
    ),
)
INFRA_BY_KEY = {i.key: i for i in INFRASTRUCTURE}
CANARY_DOUBLE_CHANCE = 0.12
CANARY_FAIL_CHANCE = 0.06
OBSERVABILITY_STEAL_CAP = 0.2


def infra_for(key: str) -> Infrastructure | None:
    return INFRA_BY_KEY.get(key)


@dataclass(frozen=True)
class DefenseTier:
    level: int
    name: str
    upgrade_cost: int
    upkeep_daily: int
    steal_fraction_floor: float
    grace_bonus: int
    steal_reduction: float = 0.0


DEFENSE_TIERS: tuple[DefenseTier, ...] = (
    DefenseTier(0, "Undefended", 0, 0, 0.10, 0, 0.00),
    DefenseTier(1, "Firewall", 5_000, 500, 0.10, 15, 0.05),
    DefenseTier(2, "WAF", 40_000, 2_500, 0.08, 30, 0.12),
    DefenseTier(3, "SOC Monitoring", 300_000, 15_000, 0.06, 60, 0.20),
    DefenseTier(4, "Zero Trust Mesh", 2_000_000, 100_000, 0.04, 120, 0.30),
)
MAX_DEFENSE_LEVEL = DEFENSE_TIERS[-1].level
DEFENSE_BY_LEVEL = {tier.level: tier for tier in DEFENSE_TIERS}
UPKEEP_WEALTH_PCT = 0.002
UPKEEP_GRACE_DAYS = 2


def defense_tier(level: int) -> DefenseTier:
    return DEFENSE_BY_LEVEL.get(max(0, level), DEFENSE_TIERS[0])


def next_defense_tier(level: int) -> DefenseTier | None:
    return DEFENSE_BY_LEVEL.get(level + 1)


def daily_upkeep(tier: DefenseTier, coins: int) -> int:
    return max(tier.upkeep_daily, round(coins * UPKEEP_WEALTH_PCT))


@dataclass(frozen=True)
class Cosmetic:
    key: str
    name: str
    icon: str
    description: str
    cost_coins: int
    kind: str
    era_key: str | None = None


COSMETICS: tuple[Cosmetic, ...] = (
    Cosmetic("title_architect", "The Architect", "\U0001f3db", "A permanent title shown on the leaderboard", 500_000, "title"),
    Cosmetic("title_refactorer", "Serial Refactorer", "♻", "A permanent title for the serially prestiged", 250_000, "title"),
    Cosmetic("title_kernel_hacker", "Kernel Hacker", "⚙", "A permanent title for Kernel harvesters", 1_000_000, "title"),
    Cosmetic("skin_neon", "Neon Terminal", "\U0001f308", "A cosmetic plot skin, no gameplay effect", 750_000, "skin"),
)
COSMETIC_BY_KEY = {c.key: c for c in COSMETICS}


def cosmetic_for(key: str) -> Cosmetic | None:
    return COSMETIC_BY_KEY.get(key)


def cosmetic_title_name(key: str) -> str:
    cosmetic = COSMETIC_BY_KEY.get(key or "")
    return cosmetic.name if cosmetic and cosmetic.kind == "title" else ""


MASTERY_UNLOCK_PRESTIGE = 50
MASTERY_PRESTIGE_STEP = 5


def _mastery_total_for(prestige: int) -> int:
    if prestige < MASTERY_UNLOCK_PRESTIGE:
        return 0
    return 1 + (prestige - MASTERY_UNLOCK_PRESTIGE) // MASTERY_PRESTIGE_STEP


def mastery_points_awarded(old_prestige: int, new_prestige: int) -> int:
    return max(0, _mastery_total_for(new_prestige) - _mastery_total_for(old_prestige))


@dataclass(frozen=True)
class MasteryUpgrade:
    key: str
    name: str
    icon: str
    description: str
    max_level: int
    base_cost: int
    cost_growth: float


MASTERY_UPGRADES: tuple[MasteryUpgrade, ...] = (
    MasteryUpgrade(
        "autoreplant",
        "Continuous Delivery",
        "\U0001f501",
        "Automatically replant the same crop right after harvest, if affordable",
        1,
        3,
        1.0,
    ),
    MasteryUpgrade(
        "analytics",
        "Farm Analytics",
        "\U0001f4ca",
        "Unlocks lifetime stats on your farm HUD",
        1,
        2,
        1.0,
    ),
    MasteryUpgrade(
        "contracts",
        "Legacy Contracts",
        "\U0001f4dc",
        "Unlocks a weekly long-term contract slot for Stars and a temporary boost",
        1,
        4,
        1.0,
    ),
)
MASTERY_BY_KEY = {m.key: m for m in MASTERY_UPGRADES}


def mastery_for(key: str) -> MasteryUpgrade | None:
    return MASTERY_BY_KEY.get(key)


def mastery_cost(m: MasteryUpgrade, level: int) -> int:
    return round(m.base_cost * (m.cost_growth ** level))


FAIR_PLAY_ACTIVITY_WEIGHT = 50
FAIR_PLAY_HOARD_DIVISOR = 200_000
MIN_RAIDS_FOR_EFFICIENCY_BOARD = 3


def fair_play_score(harvests_week: int, coins: int) -> int:
    return harvests_week * FAIR_PLAY_ACTIVITY_WEIGHT - min(coins, 10**9) // FAIR_PLAY_HOARD_DIVISOR


ERA_PRESTIGE_CARRYOVER_PCT = 0.4
ERA_REWARD_STARS_BY_RANK: tuple[int, ...] = (50, 30, 20, 15, 10, 5, 5, 5, 5, 5)


def era_score(era_coins: int, era_harvests: int, prestige: int) -> int:
    return (
        era_coins // 20
        + era_harvests * 10
        + round(prestige * SCORE_PRESTIGE * ERA_PRESTIGE_CARRYOVER_PCT)
    )


def era_reward_stars(rank: int) -> int:
    if rank < 1 or rank > len(ERA_REWARD_STARS_BY_RANK):
        return 0
    return ERA_REWARD_STARS_BY_RANK[rank - 1]
