#include "Simulation.h"

#include "FactoryChecksum.h"
#include "FactoryQueries.h"
#include "ConstructionSystem.h"
#include "DeconstructionSystem.h"
#include "PlacementRules.h"

#include <algorithm>
#include <cassert>
#include <cmath>

#include "AiSystem.h"
#include "Command.h"
#include "BuildingSystem.h"
#include "ProductionSystem.h"
#include "CombatSystem.h"
#include "DynamicBodyComponent.h"
#include "DynamicBodySystem.h"
#include "FacingComponent.h"
#include "FactionComponent.h"
#include "EventManager.h"
#include "HealthComponent.h"
#include "ModuleOwnerComponent.h"
#include "MovementIntentSystem.h"
#include "PositionComponent.h"
#include "RepairSystem.h"
#include "SalvagerSystem.h"
#include "DebrisComponent.h"
#include "DebrisSystem.h"
#include "ShipIdentityComponent.h"
#include "ShipSystem.h"
#include "StateChecksum.h"
#include "StationBodyComponent.h"
#include "SurfaceMask.h"
#include "tracing.h"
#include "WaveSystem.h"
#include "WeaponComponent.h"

Simulation::Simulation(GameConfig config, unsigned int seed)
    : m_config(std::move(config))
    , m_rng(seed)
    , m_seed(seed)
    , m_currentTick(0)
    , m_nextDepartureTick(secondsToTicks(m_config.world.departureIntervalSeconds))
    , m_gameOver(false)
    , m_hqProxyEntity(entt::null)
    , m_playerStation1Entity(entt::null)
    , m_playerStation2Entity(entt::null)
    , m_unlockState(m_config)
    , m_beltSystem(m_config.world.beltSpeed_tps)
{
    m_currentEnemyStationEntities[0] = entt::null;
    m_currentEnemyStationEntities[1] = entt::null;
    m_factoryState = makeFactoryState(m_config);

    initializeSubsystems();

    m_unlockState.initializeUnlockState();
    placeInitialStructures();
	registerForEvents();
}

Simulation::~Simulation()
{
	unregisterForEvents();
}

const GameConfig& Simulation::getConfig() const
{
    return m_config;
}

void Simulation::reset(GameConfig newConfig, unsigned int seed)
{
    m_config = std::move(newConfig);
    reset(seed);
}

void Simulation::reset(unsigned int seed)
{
	EventManager::getInstance()->clearEvents();
    m_rng.seed(seed);
    m_seed                     = seed;
    m_currentTick              = 0;
    m_nextDepartureTick        = secondsToTicks(m_config.world.departureIntervalSeconds);
    m_expansionsPurchased      = 0;
    m_gameOver                      = false;
    m_isWon                         = false;
    m_artifactCount                 = 0;
    m_hqBuildingId                  = std::nullopt;
    m_hqProxyEntity                 = entt::null;
    m_playerStation1Entity          = entt::null;
    m_playerStation2Entity          = entt::null;
    m_currentEnemyStationEntities[0] = entt::null;
    m_currentEnemyStationEntities[1] = entt::null;
    m_beamFiredEvents.clear();
    m_pendingSchematicChoices.clear();

    m_admin.clear();
    m_factoryState = makeFactoryState(m_config);
    m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
    initializeSubsystems();

    m_unlockState.initializeUnlockState();
    placeInitialStructures();
}

void Simulation::initializeSubsystems()
{
    m_buildingSystem = std::make_unique<BuildingSystem>(m_config);
    m_productionSystem = std::make_unique<ProductionSystem>(
        m_config,
        [this](const std::string& id, QVector2D pos,
               const std::optional<ShipLayoutConfig>& layout) {
            if (!isSchematicUnlocked(id))
            {
                return;
            }
            m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout);
        },
        [this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
        [this](const std::string& recipeId) -> bool { return isRecipeUnlocked(recipeId); },
        m_rng);
    m_constructionSystem = std::make_unique<ConstructionSystem>(m_config);
    m_deconstructionSystem = std::make_unique<DeconstructionSystem>(m_config);
    m_shipSystem     = std::make_unique<ShipSystem>(m_config, m_admin);
    m_aiSystem       = std::make_unique<AiSystem>(m_config);
    m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
    m_dynamicBodySystem    = std::make_unique<DynamicBodySystem>();
    m_debrisSystem    = std::make_unique<DebrisSystem>(m_admin);
    m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
    m_repairSystem   = std::make_unique<RepairSystem>(m_admin);
    m_waveSystem     = std::make_unique<WaveSystem>(m_config, m_rng);
    m_combatSystem   = std::make_unique<CombatSystem>(m_config);
}

// ---------------------------------------------------------------------------
// tick
// ---------------------------------------------------------------------------

void Simulation::apply(const Command& command)
{
    switch (command.kind)
    {
    case CommandKind::PlaceBuilding:
    {
        const PlaceBuildingCommand& c = static_cast<const PlaceBuildingCommand&>(command);
        const std::optional<BuildingId> placed = tryPlaceBuilding(c.type, c.anchor, c.rotation);
        if (!placed.has_value())
        {
            break;
        }
        const BuildingId id = *placed;
        if (c.recipeId.has_value())
        {
            m_buildingSystem->setRecipe(m_factoryState, id, *c.recipeId);
        }
        if (c.shipLayout.has_value())
        {
            m_buildingSystem->setShipLayout(m_factoryState, id, *c.shipLayout);
        }
        if (c.hasSplitterFilters)
        {
            m_buildingSystem->setSiteSplitterFilters(m_factoryState, id, c.splitterFilterA, c.splitterFilterB);
        }
        break;
    }
    case CommandKind::Deconstruct:
        deconstruct(*static_cast<const DeconstructCommand&>(command).id);
        break;
    case CommandKind::CancelDeconstruction:
        cancelDeconstruction(*static_cast<const CancelDeconstructionCommand&>(command).id);
        break;
    case CommandKind::RotateInPlace:
    {
        const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
        m_buildingSystem->rotateInPlace(m_factoryState, m_beltSystem, *c.id,
                                        c.newRotation);
        break;
    }
    case CommandKind::SetRecipe:
    {
        const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
        m_buildingSystem->setRecipe(m_factoryState, *c.id, c.recipeId);
        break;
    }
    case CommandKind::SetShipLayout:
    {
        const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
        m_buildingSystem->setShipLayout(m_factoryState, *c.id, c.layout);
        break;
    }
    case CommandKind::SetSiteSplitterFilters:
    {
        const SetSiteSplitterFiltersCommand& c =
            static_cast<const SetSiteSplitterFiltersCommand&>(command);
        m_buildingSystem->setSiteSplitterFilters(m_factoryState, *c.id, c.filterA, c.filterB);
        break;
    }
    case CommandKind::SetSplitterFilters:
    {
        const SetSplitterFiltersCommand& c =
            static_cast<const SetSplitterFiltersCommand&>(command);
        m_beltSystem.setSplitterFilters(c.tile, c.filterA, c.filterB);
        break;
    }
    case CommandKind::ClearBeltTiles:
        m_beltSystem.clearTiles(static_cast<const ClearBeltTilesCommand&>(command).tiles);
        break;
    case CommandKind::ApplySchematicChoice:
        applySchematicChoice(static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex);
        break;
    case CommandKind::ExpandAsteroid:
        tryExpandAsteroid();
        break;
    case CommandKind::Reset:
    {
        const ResetCommand& c = static_cast<const ResetCommand&>(command);
        if (c.config)
        {
            // operator* on a const shared_ptr yields a mutable GameConfig&, so
            // the move-only config moves into reset without a copy. The command
            // is applied once, so leaving its config moved-from is fine.
            reset(std::move(*c.config), c.seed);
        }
        else
        {
            reset(c.seed);
        }
        break;
    }
    }
}

void Simulation::tick()
{
	EventManager::getInstance()->processEvents();

    // Step 1: wave scheduler
    m_waveSystem->tickWaveScheduler(m_currentTick, *m_shipSystem,
                                    m_config.world.heightTiles);

    // Step 2: threat accumulation
    m_waveSystem->tickThreatAccumulation();

    // Construction + production pipeline
    m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick);
    m_deconstructionSystem->tick(m_factoryState, m_currentTick);        // parallel to construction
    m_productionSystem->tickBeltPull(m_factoryState, m_beltSystem);      // step 3
    m_productionSystem->tickProduction(m_factoryState, m_currentTick);   // step 4
    m_productionSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b
    m_productionSystem->tickOutputBelts(m_factoryState, m_beltSystem);   // step 5
    m_beltSystem.tick();                                 // step 6

    // Step 7: ship behavior systems (movement arbitration via intent priority)

    // Departure timer: release gathered combat ships on a fixed interval (REQ-SHP-RALLY).
    if (m_currentTick >= m_nextDepartureTick)
    {
        m_shipSystem->triggerRallyDeparture();
        m_nextDepartureTick += secondsToTicks(m_config.world.departureIntervalSeconds);
    }

    m_shipSystem->clearMovementIntents();
    // Score-based behavior selection: evaluate, select winner, execute (sets
    // movement intent + preferred module targets only — no world mutation).
    m_aiSystem->tick(m_admin, m_factoryState);
    // Module systems perform the world mutation (collection/delivery, healing).
    // Each emits its tool beams and applies its own delayed (mid-beam) effects.
    m_salvagerSystem->tick(m_currentTick, m_factoryState, m_beamFiredEvents);
    m_repairSystem->tick(m_currentTick, m_beamFiredEvents);

    // Step 8: combat resolution
    m_combatSystem->tick(m_currentTick, m_admin, m_beamFiredEvents);

    // Step 8b: deferred damage whose impact tick has arrived
    m_combatSystem->applyPendingDamage(m_currentTick, m_admin);

    // Step 9: deaths & loot
    if (!m_gameOver)
    {
        tickDeathsAndLoot();
    }

    // Step 10: advance ship positions
    m_movementIntentSystem->tick(m_admin);
    m_dynamicBodySystem->tick(m_admin);

    // Step 11: debris despawn
    m_debrisSystem->tickDespawn(m_currentTick);

    ++m_currentTick;
}

// ---------------------------------------------------------------------------
// Pre-placement
// ---------------------------------------------------------------------------

void Simulation::placeInitialStructures()
{
    // HQ — right edge of asteroid (rightmost asteroid tile is x = -1).
    // Placed as a Building (for belt input) plus an ECS proxy (for HP/targeting).
    const ParsedSurfaceMask hqParsed =
        parseSurfaceMask(m_config.stations.hq.surfaceMask, Rotation::East);
    const int hqAnchorX = -hqParsed.footprint.width();
    const int hqAnchorY =
        (m_config.world.heightTiles - hqParsed.footprint.height()) / 2;
    const float hqHp =
        static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0));
    m_hqBuildingId = m_buildingSystem->placeImmediate(m_factoryState, BuildingType::Hq,
        m_config.stations.hq.surfaceMask,
        QPoint(hqAnchorX, hqAnchorY),
        Rotation::East);

    const QVector2D hqCenter(
        hqAnchorX + hqParsed.footprint.width() / 2.0f,
        hqAnchorY + hqParsed.footprint.height() / 2.0f);
    m_hqProxyEntity = m_admin.spawnHqProxy(hqCenter, hqHp, hqHp);

    // Player defence stations — ECS entities with tile occupancy.
    const ParsedSurfaceMask psParsed =
        parseSurfaceMask(m_config.stations.playerStation.surfaceMask, Rotation::East);
    const int psAnchorX =
        m_config.world.regions.playerBufferWidth_tiles - psParsed.footprint.width();
    const double psLevel = static_cast<double>(m_config.stations.playerStation.level);
    const float psHp = static_cast<float>(
        m_config.stations.playerStation.hpFormula.evaluate(psLevel));

    const float tileSize = static_cast<float>(m_config.world.tileSize_m);

    WeaponComponent psWeapon;
    psWeapon.damage        = static_cast<float>(
        m_config.stations.playerStation.damageFormula.evaluate(psLevel));
    psWeapon.range_tiles   = static_cast<float>(
        m_config.stations.playerStation.rangeFormula.evaluate(psLevel)) / tileSize;
    psWeapon.fireRateHz    = static_cast<float>(
        m_config.stations.playerStation.fireRateFormula.evaluate(psLevel));
    psWeapon.cooldownTicks  = 0.0f;
    psWeapon.currentTarget  = std::nullopt;

    const int ps1Y = m_config.world.heightTiles / 4;
    const int ps2Y = 3 * m_config.world.heightTiles / 4;

    {
        const QPoint anchor(psAnchorX, ps1Y);
        std::vector<QPoint> absCells;
        for (const QPoint& rel : psParsed.bodyCells)
        {
            absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
        }
        m_playerStation1Entity = m_admin.spawnStation(
            anchor, psParsed.footprint, absCells, psHp, psHp, false);
        {
            entt::entity wChild = m_admin.createModuleEntity();
            m_admin.addComponent<WeaponComponent>(wChild, psWeapon);
            m_admin.addComponent<ModuleOwnerComponent>(wChild,
                ModuleOwnerComponent{m_playerStation1Entity});
        }
        m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
                                                allocateBuildingId(m_factoryState));
    }
    {
        const QPoint anchor(psAnchorX, ps2Y);
        std::vector<QPoint> absCells;
        for (const QPoint& rel : psParsed.bodyCells)
        {
            absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
        }
        m_playerStation2Entity = m_admin.spawnStation(
            anchor, psParsed.footprint, absCells, psHp, psHp, false);
        {
            entt::entity wChild = m_admin.createModuleEntity();
            m_admin.addComponent<WeaponComponent>(wChild, psWeapon);
            m_admin.addComponent<ModuleOwnerComponent>(wChild,
                ModuleOwnerComponent{m_playerStation2Entity});
        }
        m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
                                                allocateBuildingId(m_factoryState));
    }

    // Rally point: center of the player defence stations' X column, world vertical midpoint.
    const float rallyX = static_cast<float>(psAnchorX) + psParsed.footprint.width() / 2.0f;
    const float rallyY = static_cast<float>(m_config.world.heightTiles) / 2.0f;
    m_shipSystem->setRallyPoint(QVector2D(rallyX, rallyY));

    // Enemy defence stations — generation 0 (initial set).
    placeEnemyStationSet(0);
}

void Simulation::placeEnemyStationSet(int generation)
{
    const float tileSize = static_cast<float>(m_config.world.tileSize_m);
    const ParsedSurfaceMask esParsed =
        parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East);

    const int rightEdgeX = m_config.world.regions.playerBufferWidth_tiles
                         + m_config.world.regions.contestZoneWidth_tiles
                         + generation * m_config.world.push.pushExpandColumns_tiles;
    const int anchorX = rightEdgeX - esParsed.footprint.width();

    const double genD = static_cast<double>(generation);
    const float esHp  = static_cast<float>(
        m_config.stations.enemyStation.hpFormula.evaluate(genD));

    WeaponComponent esWeapon;
    esWeapon.damage        = static_cast<float>(
        m_config.stations.enemyStation.damageFormula.evaluate(genD));
    esWeapon.range_tiles   = static_cast<float>(
        m_config.stations.enemyStation.rangeFormula.evaluate(genD)) / tileSize;
    esWeapon.fireRateHz    = static_cast<float>(
        m_config.stations.enemyStation.fireRateFormula.evaluate(genD));
    esWeapon.cooldownTicks  = 0.0f;
    esWeapon.currentTarget  = std::nullopt;

    const int y1 = m_config.world.heightTiles / 4;
    const int y2 = 3 * m_config.world.heightTiles / 4;

    {
        const QPoint anchor(anchorX, y1);
        std::vector<QPoint> absCells;
        for (const QPoint& rel : esParsed.bodyCells)
        {
            absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
        }
        m_currentEnemyStationEntities[0] = m_admin.spawnStation(
            anchor, esParsed.footprint, absCells, esHp, esHp, true);
        {
            entt::entity wChild = m_admin.createModuleEntity();
            m_admin.addComponent<WeaponComponent>(wChild, esWeapon);
            m_admin.addComponent<ModuleOwnerComponent>(wChild,
                ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
        }
        m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
                                                allocateBuildingId(m_factoryState));
    }
    {
        const QPoint anchor(anchorX, y2);
        std::vector<QPoint> absCells;
        for (const QPoint& rel : esParsed.bodyCells)
        {
            absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
        }
        m_currentEnemyStationEntities[1] = m_admin.spawnStation(
            anchor, esParsed.footprint, absCells, esHp, esHp, true);
        {
            entt::entity wChild = m_admin.createModuleEntity();
            m_admin.addComponent<WeaponComponent>(wChild, esWeapon);
            m_admin.addComponent<ModuleOwnerComponent>(wChild,
                ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
        }
        m_buildingSystem->registerTileOccupancy(m_factoryState, absCells,
                                                allocateBuildingId(m_factoryState));
    }
}

// ---------------------------------------------------------------------------
// Deaths & loot (tick step 9)
// ---------------------------------------------------------------------------

void Simulation::tickDeathsAndLoot()
{
	TRACE();
    // --- Dead ships ---
    std::vector<entt::entity> deadShips;
    m_admin.forEach<ShipIdentityComponent, HealthComponent>(
        [&deadShips](entt::entity e, const ShipIdentityComponent& /*si*/,
                     const HealthComponent& h)
        {
            if (h.hp <= 0.0f)
            {
                deadShips.push_back(e);
            }
        });

    for (entt::entity deadEntity : deadShips)
    {
        const ShipIdentityComponent& si = m_admin.get<ShipIdentityComponent>(deadEntity);
        const PositionComponent& pos    = m_admin.get<PositionComponent>(deadEntity);
        if (si.scrapDrop > 0)
        {
            const Tick despawnAt = m_currentTick
                + secondsToTicks(m_config.world.debrisDespawnSeconds);
            m_debrisSystem->spawn(pos.value, si.scrapDrop, despawnAt);
        }
        m_shipSystem->despawn(deadEntity);
    }

    // --- Dead stations ---
    std::vector<entt::entity> deadStations;
    m_admin.forEach<StationBodyComponent, HealthComponent>(
        [&deadStations](entt::entity e, const StationBodyComponent& /*sb*/,
                        const HealthComponent& h)
        {
            if (h.hp <= 0.0f)
            {
                deadStations.push_back(e);
            }
        });

    for (entt::entity deadEntity : deadStations)
    {
        const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
        const PositionComponent& pos   = m_admin.get<PositionComponent>(deadEntity);
        const FactionComponent& fac    = m_admin.get<FactionComponent>(deadEntity);

        const Tick despawnAt = m_currentTick
            + secondsToTicks(m_config.world.debrisDespawnSeconds);
        int scrap = 0;
        if (!fac.isEnemy)
        {
            const double lv = static_cast<double>(
                m_config.stations.playerStation.level);
            scrap = static_cast<int>(
                m_config.stations.playerStation.scrapDropFormula.evaluate(lv));
        }
        else
        {
            const double genD = static_cast<double>(m_waveSystem->getGeneration());
            scrap = static_cast<int>(
                m_config.stations.enemyStation.scrapDropFormula.evaluate(genD));
        }
        if (scrap > 0)
        {
            m_debrisSystem->spawn(pos.value, scrap, despawnAt);
        }
        m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
        {
            std::vector<entt::entity> stationChildren;
            m_admin.forEach<ModuleOwnerComponent>(
                [&](entt::entity ce, const ModuleOwnerComponent& o)
                {
                    if (o.owner == deadEntity) { stationChildren.push_back(ce); }
                });
            for (entt::entity ce : stationChildren) { m_admin.destroy(ce); }
        }
        m_admin.destroy(deadEntity);
    }

    // --- HQ death check ---
    if (m_admin.isValid(m_hqProxyEntity))
    {
        const HealthComponent& hqHealth = m_admin.get<HealthComponent>(m_hqProxyEntity);
        if (hqHealth.hp <= 0.0f)
        {
            m_gameOver = true;
        }
    }

    // --- Push check: if both current enemy stations are gone, trigger push ---
    const bool es0Gone = !m_admin.isValid(m_currentEnemyStationEntities[0])
        || m_admin.get<HealthComponent>(m_currentEnemyStationEntities[0]).hp <= 0.0f;
    const bool es1Gone = !m_admin.isValid(m_currentEnemyStationEntities[1])
        || m_admin.get<HealthComponent>(m_currentEnemyStationEntities[1]).hp <= 0.0f;

    if (es0Gone && es1Gone &&
        m_currentEnemyStationEntities[0] != entt::null)
    {
        const int destroyedLevel = m_waveSystem->getGeneration();
        m_waveSystem->onEnemyStationsDestroyed();
        placeEnemyStationSet(m_waveSystem->getGeneration());
        generateSchematicChoices(destroyedLevel);
    }
}

void Simulation::generateSchematicChoices(int destroyedStationLevel)
{
    // Build the eligible pool of unlock groups (REQ-DEF-SCHEMATIC-DROP,
    // REQ-LOCK-EXPLICIT, REQ-LOCK-PREREQ): not yet awarded, station level in
    // range, and every prerequisite group already awarded.
    std::vector<const UnlockGroupDef*> pool;
    for (const UnlockGroupDef& group : m_config.unlocks.groups)
    {
        if (m_unlockState.isUnlockGroupAwarded(group.id)) { continue; }
        if (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; }
        if (!m_unlockState.prerequisitesSatisfied(group.requiredGroupIds)) { continue; }
        pool.push_back(&group);
    }

    if (pool.empty()) { return; }

    const double artifactChance = std::clamp(
        m_config.world.artifacts.artifactChanceFormula.evaluate(
            static_cast<double>(destroyedStationLevel)),
        0.0, 1.0);
    std::uniform_real_distribution<double> realDist(0.0, 1.0);
    const bool artifactRolled = realDist(m_rng) < artifactChance;

    const int numChoices = std::min(static_cast<int>(pool.size()), artifactRolled ? 2 : 3);
    m_pendingSchematicChoices.clear();

    for (int i = 0; i < numChoices; ++i)
    {
        std::uniform_int_distribution<int> dist(0, static_cast<int>(pool.size()) - 1 - i);
        const int roll = dist(m_rng);
        const std::size_t rollIdx = static_cast<std::size_t>(roll);
        const std::size_t endIdx  = pool.size() - 1 - static_cast<std::size_t>(i);
        std::swap(pool[rollIdx], pool[endIdx]);

        m_pendingSchematicChoices.push_back(m_unlockState.makeUnlockOption(*pool[endIdx]));
    }

    if (artifactRolled)
    {
        SchematicChoiceOption artifactOption;
        artifactOption.isArtifact  = true;
        artifactOption.displayName = "Artifact";
        m_pendingSchematicChoices.push_back(std::move(artifactOption));
    }
}

void Simulation::applySchematicChoice(int choiceIndex)
{
    assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
    const SchematicChoiceOption& chosen = m_pendingSchematicChoices[static_cast<std::size_t>(choiceIndex)];

    if (chosen.isArtifact)
    {
        m_artifactCount += 1;
        if (m_artifactCount >= m_config.world.artifacts.artifactWinCount)
        {
            m_isWon = true;
        }
        m_pendingSchematicChoices.clear();
        return;
    }

    m_unlockState.awardUnlockGroup(chosen);
    m_pendingSchematicChoices.clear();
}

bool Simulation::isRecipeUnlocked(const std::string& recipeId) const
{
    return m_unlockState.isRecipeUnlocked(recipeId);
}

bool Simulation::isItemUnlocked(const std::string& itemId) const
{
    return m_unlockState.isItemUnlocked(itemId);
}

// ---------------------------------------------------------------------------
// Determinism (see docs/replay_design.md)
// ---------------------------------------------------------------------------

unsigned long long Simulation::getRngFingerprint() const
{
    return fingerprintRng(m_rng);
}

unsigned long long Simulation::computeStateChecksum() const
{
    Hasher hasher;

    // RNG stream — the most sensitive signal of divergence.
    hasher.append(fingerprintRng(m_rng));

    // Top-level scalars.
    hasher.append(m_currentTick);
    hasher.append(m_nextDepartureTick);
    hasher.append(m_factoryState.nextBuildingId);
    hasher.append(m_factoryState.buildingBlocksStock);
    hasher.append(m_gameOver);
    hasher.append(m_isWon);
    hasher.append(m_artifactCount);
    hasher.append(m_expansionsPurchased);

    // WaveSystem scalar state, reached through existing accessors.
    hasher.append(getThreatLevel());
    hasher.append(getThreatAccumulationRate());
    hasher.append(getBossWaveCounter());
    hasher.append(getBossCountdownTicks());
    hasher.append(getNormalGapRemainingTicks());

    // Schematic / unlock state (std::map and std::set iterate in sorted order).
    m_unlockState.appendChecksum(hasher);

    // Subsystems contribute their own state.
    appendChecksum(m_factoryState, hasher);
    m_beltSystem.appendChecksum(hasher);

    // ECS component state. View iteration order is a pure function of the
    // (identical) operation sequence on a fixed binary; each entity's raw id is
    // folded in so the fingerprint is keyed, not merely a sum of fields.
    m_admin.forEach<PositionComponent>(
        [&hasher](entt::entity entity, const PositionComponent& c)
    {
        hasher.append(static_cast<std::uint32_t>(entity));
        hasher.append(c.value);
    });
    m_admin.forEach<HealthComponent>(
        [&hasher](entt::entity entity, const HealthComponent& c)
    {
        hasher.append(static_cast<std::uint32_t>(entity));
        hasher.append(c.hp);
        hasher.append(c.maxHp);
    });
    m_admin.forEach<FacingComponent>(
        [&hasher](entt::entity entity, const FacingComponent& c)
    {
        hasher.append(static_cast<std::uint32_t>(entity));
        hasher.append(c.radians);
    });
    m_admin.forEach<DynamicBodyComponent>(
        [&hasher](entt::entity entity, const DynamicBodyComponent& c)
    {
        hasher.append(static_cast<std::uint32_t>(entity));
        hasher.append(c.velocity_tpt);
        hasher.append(c.angularVelocity_rpt);
        hasher.append(c.linearAcceleration_tptt);
        hasher.append(c.angularAcceleration_rptt);
    });
    m_admin.forEach<DebrisComponent>(
        [&hasher](entt::entity entity, const DebrisComponent& c)
    {
        hasher.append(static_cast<std::uint32_t>(entity));
        hasher.append(c.amount);
    });
    m_admin.forEach<ShipIdentityComponent>(
        [&hasher](entt::entity entity, const ShipIdentityComponent& c)
    {
        hasher.append(static_cast<std::uint32_t>(entity));
        hasher.append(c.schematicId);
    });

    return hasher.getValue();
}

// ---------------------------------------------------------------------------
// Drains
// ---------------------------------------------------------------------------

std::vector<BeamFiredEvent> Simulation::drainBeamFiredEvents()
{
    std::vector<BeamFiredEvent> result;
    result.swap(m_beamFiredEvents);
    return result;
}

const std::vector<SchematicChoiceOption>& Simulation::getPendingSchematicChoices() const
{
    return m_pendingSchematicChoices;
}

bool Simulation::hasSchematicChoicesPending() const
{
    return !m_pendingSchematicChoices.empty();
}


// ---------------------------------------------------------------------------
// Accessors
// ---------------------------------------------------------------------------

Tick Simulation::getCurrentTick() const
{
    return m_currentTick;
}

unsigned int Simulation::getSeed() const
{
    return m_seed;
}

int Simulation::getBuildingBlocksStock() const
{
    return m_factoryState.buildingBlocksStock;
}

int Simulation::getCurrentAsteroidWidth_tiles() const
{
    return m_config.world.regions.asteroidWidth_tiles
        + m_expansionsPurchased * m_config.world.expansion.columnsPerExpansion_tiles;
}

int Simulation::getCurrentExpansionCost() const
{
    const double cost = m_config.world.expansion.costBuildingBlocksFormula.evaluate(
        static_cast<double>(m_expansionsPurchased));
    return static_cast<int>(std::floor(cost));
}

void Simulation::tryExpandAsteroid()
{
    const int cost = getCurrentExpansionCost();
    if (m_factoryState.buildingBlocksStock < cost)
    {
        return;
    }
    m_factoryState.buildingBlocksStock -= cost;
    ++m_expansionsPurchased;
    m_buildingSystem->setAsteroidWidth_tiles(m_factoryState, getCurrentAsteroidWidth_tiles());
}

bool Simulation::isGameOver() const
{
    return m_gameOver;
}

bool Simulation::isWon() const
{
    return m_isWon;
}

int Simulation::getArtifactCount() const
{
    return m_artifactCount;
}

double Simulation::getThreatLevel() const
{
    return m_waveSystem->getThreatLevel();
}

double Simulation::getThreatAccumulationRate() const
{
    return m_waveSystem->getThreatAccumulationRate();
}

double Simulation::getMaxFactoryProductionThreatRate() const
{
    return static_cast<double>(getProductionBuildingCount(m_factoryState));
}

double Simulation::getCurrentFactoryProductionThreatRate() const
{
    return static_cast<double>(getActiveProductionBuildingCount(m_factoryState));
}

int Simulation::getBossWaveCounter() const
{
    return m_waveSystem->getBossWaveCounter();
}

Tick Simulation::getBossCountdownTicks() const
{
    return m_waveSystem->getBossCountdownTicks();
}

Tick Simulation::getNormalGapRemainingTicks() const
{
    return m_waveSystem->getNormalGapRemainingTicks();
}

bool Simulation::isSchematicUnlocked(const std::string& shipId) const
{
    return m_unlockState.isSchematicUnlocked(shipId);
}

bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
{
    return m_unlockState.isModuleSchematicUnlocked(moduleId);
}

bool Simulation::isBuildingUnlocked(BuildingType type) const
{
    return m_unlockState.isBuildingUnlocked(type);
}

std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
{
    // Locked building types cannot be placed (REQ-LOCK-BUILDING); the build menu
    // hides them, this is the simulation-side backstop (e.g. blueprint placement).
    if (!isBuildingUnlocked(type))
    {
        return std::nullopt;
    }

    if (!isPlacementValid(m_factoryState, m_config, type, anchor, rotation))
    {
        return std::nullopt;
    }

    int cost = 0;
    for (const BuildingDef& def : m_config.buildings.buildings)
    {
        if (def.type == type)
        {
            cost = def.cost;
            break;
        }
    }
    if (m_factoryState.buildingBlocksStock < cost)
    {
        return std::nullopt;
    }
    m_factoryState.buildingBlocksStock -= cost;
    return m_buildingSystem->place(m_factoryState, type, anchor, rotation, m_currentTick);
}

void Simulation::deconstruct(BuildingId id)
{
    m_buildingSystem->deconstruct(m_factoryState, m_beltSystem, id, m_currentTick);
}

void Simulation::cancelDeconstruction(BuildingId id)
{
    m_buildingSystem->cancelDeconstruction(m_factoryState, m_beltSystem, id);
}

BuildingSystem& Simulation::getBuildingsMutable()
{
    return *m_buildingSystem;
}

const FactoryState& Simulation::getFactoryState() const
{
    return m_factoryState;
}

BeltSystem& Simulation::getBeltsMutable()
{
    return m_beltSystem;
}

const BeltSystem& Simulation::getBelts() const
{
    return m_beltSystem;
}

ShipSystem& Simulation::getShips()
{
    return *m_shipSystem;
}

const ShipSystem& Simulation::getShips() const
{
    return *m_shipSystem;
}

DebrisSystem& Simulation::getDebrisSystem()
{
    return *m_debrisSystem;
}

const DebrisSystem& Simulation::getDebrisSystem() const
{
    return *m_debrisSystem;
}

EntityAdmin& Simulation::getAdmin()
{
    return m_admin;
}

const EntityAdmin& Simulation::getAdmin() const
{
    return m_admin;
}

void Simulation::handleEvent(std::shared_ptr<const TracePrintRequestedEvent> event)
{
	PRINT_TRACES();
}

