#include "BuildingSystem.h"

#include <algorithm>
#include <cassert>
#include <limits>
#include <random>
#include <set>

#include "PortGeometry.h"
#include "StateChecksum.h"
#include "SurfaceMask.h"
#include "tracing.h"

namespace
{
// An input belt accepts a new item at progress 0.0 only when it holds fewer than
// three items and the entry slot is clear (nothing within a quarter tile of 0.0),
// matching the belt packing used elsewhere (REQ-GW-BELT-CAPACITY).
bool inputLaneEntryFree(const std::vector<BeltItemSlot>& lane)
{
    return lane.size() < 3 && (lane.empty() || lane.back().progress >= 0.25);
}
}  // namespace

BuildingSystem::BuildingSystem(const GameConfig& config,
                               BeltSystem& belts,
                               std::function<BuildingId()> allocateBuildingId,
                               std::function<void(int)> addBuildingBlocks,
                               std::function<void(const std::string&, QVector2D,
                                                const std::optional<ShipLayoutConfig>&)> spawnShip,
                               std::function<bool(const std::string&)> isItemUnlocked,
                               std::mt19937& rng)
    : m_config(config)
    , m_belts(belts)
    , m_allocateBuildingId(std::move(allocateBuildingId))
    , m_addBuildingBlocks(std::move(addBuildingBlocks))
    , m_spawnShip(std::move(spawnShip))
    , m_isItemUnlocked(std::move(isItemUnlocked))
    , m_rng(rng)
    , m_asteroidWidth_tiles(config.world.regions.asteroidWidth_tiles)
{
}

// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------

void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
{
    b.inputBuffer.counts.clear();
    b.inputBuffer.caps.clear();
    for (const RecipeIngredient& ing : recipe.inputs)
    {
        const ItemType type{ing.item};
        b.inputBuffer.counts[type] = 0;
        b.inputBuffer.caps[type]   = 2 * ing.amount;
    }

    b.outputBuffer.items.clear();
    if (b.type == BuildingType::ReprocessingPlant)
    {
        // 1× max-per-roll (REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
        int maxAmount = 0;
        for (const RecipeOutput& out : recipe.outputs)
        {
            if (out.amount > maxAmount)
            {
                maxAmount = out.amount;
            }
        }
        b.outputBuffer.capacity = maxAmount;
    }
    else
    {
        // 2× per-cycle output.
        int totalAmount = 0;
        for (const RecipeOutput& out : recipe.outputs)
        {
            totalAmount += out.amount;
        }
        b.outputBuffer.capacity = 2 * totalAmount;
    }
}

void BuildingSystem::initAutoBuffers(Building& b) const
{
    b.inputBuffer.counts.clear();
    b.inputBuffer.caps.clear();

    // Union the inputs of every recipe of this building type; the cap for each
    // item is twice the largest per-cycle requirement across those recipes.
    // Output capacity follows the same rules as initBuffers: the Reprocessing
    // Plant holds one cycle's max output (REQ-MAT-OUTPUT-BUFFER-REPROCESSING),
    // other auto buildings hold twice the largest per-cycle output.
    int outputCapacity = 0;
    for (const RecipeDef& recipe : m_config.recipes.recipes)
    {
        if (recipe.building != b.type)
        {
            continue;
        }

        for (const RecipeIngredient& ing : recipe.inputs)
        {
            const ItemType type{ing.item};
            b.inputBuffer.counts[type] = 0;
            b.inputBuffer.caps[type] =
                std::max(b.inputBuffer.caps[type], 2 * ing.amount);
        }

        if (b.type == BuildingType::ReprocessingPlant)
        {
            int maxAmount = 0;
            for (const RecipeOutput& out : recipe.outputs)
            {
                maxAmount = std::max(maxAmount, out.amount);
            }
            outputCapacity = std::max(outputCapacity, maxAmount);
        }
        else
        {
            int totalAmount = 0;
            for (const RecipeOutput& out : recipe.outputs)
            {
                totalAmount += out.amount;
            }
            outputCapacity = std::max(outputCapacity, 2 * totalAmount);
        }
    }

    b.outputBuffer.items.clear();
    b.outputBuffer.capacity = outputCapacity;
}

void BuildingSystem::initShipyardBuffers(Building& b) const
{
    b.inputBuffer.counts.clear();
    b.inputBuffer.caps.clear();
    b.outputBuffer.items.clear();
    b.outputBuffer.capacity = 0;
    const ShipDef* def = m_config.ships.findShipDef(b.recipeId);
    if (!def)
    {
        return;
    }
    for (const RecipeIngredient& ing : def->schematic.materials)
    {
        const ItemType type{ing.item};
        b.inputBuffer.counts[type] = 0;
        b.inputBuffer.caps[type]   = 2 * ing.amount;
    }
    if (b.shipLayout.has_value())
    {
        for (const PlacedModule& pm : b.shipLayout->placedModules)
        {
            const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
            if (!modDef)
            {
                continue;
            }
            for (const RecipeIngredient& ing : modDef->materials)
            {
                const ItemType type{ing.item};
                b.inputBuffer.counts.try_emplace(type, 0);
                b.inputBuffer.caps[type] += 2 * ing.amount;
            }
        }
    }
}

void BuildingSystem::initSalvageBayBuffer(Building& b) const
{
    // Salvage Bay has no recipe-driven buffer; its output-buffer holding size for
    // ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY).
    b.outputBuffer.items.clear();
    const BuildingDef* def = m_config.buildings.findBuildingDef(BuildingType::SalvageBay);
    b.outputBuffer.capacity =
        (def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
}

std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
{
    return computeInputPorts(b.bodyCells, b.outputPorts);
}

std::vector<Port> BuildingSystem::computeInputPorts(
    const std::vector<QPoint>& bodyCells,
    const std::vector<Port>& outputPorts) const
{
    // Build lookup sets for quick membership checks.
    std::set<std::pair<int, int>> bodySet;
    for (const QPoint& cell : bodyCells)
    {
        bodySet.insert({cell.x(), cell.y()});
    }

    std::set<std::pair<int, int>> outputPortTiles;
    for (const Port& port : outputPorts)
    {
        outputPortTiles.insert({port.tile.x(), port.tile.y()});
    }

    // Neighbour deltas and the corresponding "inward" belt direction.
    const int dx[4]            = {-1,  1,  0,  0};
    const int dy[4]            = { 0,  0, -1,  1};
    const Rotation inward[4]   = {
        Rotation::East,   // neighbour is to the West; belt flows East toward building
        Rotation::West,   // neighbour is to the East; belt flows West toward building
        Rotation::South,  // neighbour is above (row-1); belt flows South toward building
        Rotation::North   // neighbour is below (row+1); belt flows North toward building
    };

    std::set<std::pair<int, int>> seen;
    std::vector<Port> inputPorts;

    for (const QPoint& cell : bodyCells)
    {
        for (int i = 0; i < 4; ++i)
        {
            const int nx = cell.x() + dx[i];
            const int ny = cell.y() + dy[i];
            const std::pair<int, int> neighbor = {nx, ny};

            if (bodySet.count(neighbor))       { continue; }
            if (outputPortTiles.count(neighbor)){ continue; }
            if (seen.count(neighbor))           { continue; }

            seen.insert(neighbor);
            Port port;
            port.tile      = QPoint(nx, ny);
            port.direction = inward[i];
            inputPorts.push_back(port);
        }
    }

    return inputPorts;
}

std::vector<Port> BuildingSystem::getInputPorts(BuildingId id) const
{
    if (const Building* building = findBuilding(id))
    {
        return building->inputPorts;
    }
    if (const ConstructionSite* site = findSite(id))
    {
        // A site stores no ports; derive its output ports from the mask (absolute)
        // and run the same input-edge scan (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS).
        const BuildingDef* def = m_config.buildings.findBuildingDef(site->type);
        if (def == nullptr) { return {}; }
        const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, site->rotation);
        std::vector<Port> outputPortsAbsolute;
        outputPortsAbsolute.reserve(mask.outputPorts.size());
        for (const Port& port : mask.outputPorts)
        {
            outputPortsAbsolute.push_back(Port{ site->anchor + port.tile, port.direction });
        }
        return computeInputPorts(site->bodyCells, outputPortsAbsolute);
    }
    return {};
}

std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
{
    std::vector<const RecipeOutput*> eligible;
    std::vector<double> weights;
    for (const RecipeOutput& out : recipe.outputs)
    {
        if (!m_isItemUnlocked(out.item)) { continue; }
        eligible.push_back(&out);
        weights.push_back(out.probability.value_or(1.0));
    }

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

    std::discrete_distribution<int> dist(weights.begin(), weights.end());
    const RecipeOutput& chosen = *eligible[static_cast<std::size_t>(dist(m_rng))];
    std::vector<Item> result;
    Item item;
    item.type.id = chosen.item;
    for (int i = 0; i < chosen.amount; ++i)
    {
        result.push_back(item);
    }
    return result;
}

// ---------------------------------------------------------------------------
// Placement
// ---------------------------------------------------------------------------

std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor,
                                Rotation rotation, Tick currentTick)
{
    const BuildingDef* def = m_config.buildings.findBuildingDef(type);
    assert(def != nullptr);
    const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);

    // Reject placements that fall outside the world (REQ-BLD-PLACE-VALID).
    if (!bodyCellsWithinWorldBounds(mask.bodyCells, anchor))
    {
        return std::nullopt;
    }

    const BuildingId id = m_allocateBuildingId();

    // Record tile occupancy for body cells.
    for (const QPoint& cell : mask.bodyCells)
    {
        const QPoint absCell = anchor + cell;
        m_state.grid.occupy(absCell, id);
    }

    // Build construction site.
    ConstructionSite site;
    site.id       = id;
    site.anchor   = anchor;
    site.footprint = mask.footprint;
    site.rotation = rotation;
    site.type     = type;
    for (const QPoint& cell : mask.bodyCells)
    {
        site.bodyCells.push_back(anchor + cell);
    }

    if (m_state.constructionQueue.empty())
    {
        site.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
    }
    // else: completesAt remains 0 (queued, not yet started).

    m_state.constructionQueue.push_back(std::move(site));
    return id;
}

bool BuildingSystem::bodyCellsWithinWorldBounds(const std::vector<QPoint>& bodyCells,
                                                QPoint anchor) const
{
    const int heightTiles = m_config.world.heightTiles;
    const int leftEdgeX   = -m_asteroidWidth_tiles;
    for (const QPoint& cell : bodyCells)
    {
        const QPoint worldCell = anchor + cell;
        if (worldCell.y() < 0 || worldCell.y() >= heightTiles)
        {
            return false;
        }
        if (worldCell.x() < leftEdgeX)
        {
            return false;
        }
    }
    return true;
}

bool BuildingSystem::isPlacementValid(BuildingType type, QPoint anchor,
                                      Rotation rotation) const
{
    const BuildingDef* def = m_config.buildings.findBuildingDef(type);
    if (def == nullptr)
    {
        return false;
    }
    const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);

    if (!bodyCellsWithinWorldBounds(mask.bodyCells, anchor))
    {
        return false;
    }

    // Terrain: ship-dock (S) cells must sit in space (x >= 0); all other body
    // (A) cells must sit on the asteroid (x < 0). (REQ-BLD-PLACE-VALID)
    for (const QPoint& cell : mask.bodyCells)
    {
        const QPoint worldCell = anchor + cell;
        bool isShipDock = false;
        for (const QPoint& dock : mask.shipDockCells)
        {
            if (dock == cell)
            {
                isShipDock = true;
                break;
            }
        }
        if (isShipDock)
        {
            if (worldCell.x() < 0)
            {
                return false;
            }
        }
        else if (worldCell.x() >= 0)
        {
            return false;
        }
    }
    return true;
}

// ---------------------------------------------------------------------------
// Deconstruct
// ---------------------------------------------------------------------------

int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
{
    // Construction site? Removed instantly with the full refund; never queued
    // for deconstruction (REQ-BLD-DECONSTRUCT).
    for (std::deque<ConstructionSite>::iterator it = m_state.constructionQueue.begin();
         it != m_state.constructionQueue.end();
         ++it)
    {
        if (it->id == id)
        {
            const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
            m_state.grid.release(it->bodyCells);
            m_state.constructionQueue.erase(it);
            if (def)
            {
                return def->cost;
            }
            return 0;
        }
    }

    // Operational building? Append it to the deconstruction queue rather than
    // removing it now; the partial refund is credited on completion in
    // tickDeconstruction (REQ-BLD-DECON-QUEUE).
    for (Building& building : m_state.buildings)
    {
        if (building.id != id) { continue; }
        if (building.queuedForDeconstruction) { return 0; }  // already queued

        building.queuedForDeconstruction = true;

        DeconstructionEntry entry;
        entry.id = id;

        // A queued belt/tunnel/splitter stops transporting at once: capture a
        // splitter's filters (so an un-queue can restore them), then unregister
        // its tile, discarding items on it and re-pairing tunnels as if it were
        // gone (REQ-BLD-TUNNEL-PAIR).
        if (building.type == BuildingType::Splitter)
        {
            if (const std::optional<BeltSystem::SplitterInfo> info =
                    m_belts.getSplitterInfo(building.anchor))
            {
                entry.splitterFilterA = info->filterA;
                entry.splitterFilterB = info->filterB;
            }
        }
        if (isBeltSubsystemType(building.type))
        {
            m_belts.removeTile(building.anchor);
        }

        const bool wasEmpty = m_state.deconstructionQueue.empty();
        m_state.deconstructionQueue.push_back(std::move(entry));
        if (wasEmpty)
        {
            startFrontDeconstruction(currentTick);
        }
        return 0;
    }

    return 0;
}

void BuildingSystem::startFrontDeconstruction(Tick currentTick)
{
    if (m_state.deconstructionQueue.empty()) { return; }
    DeconstructionEntry& front = m_state.deconstructionQueue.front();
    if (front.completesAt == 0)
    {
        front.completesAt =
            currentTick + secondsToTicks(m_config.world.deconstructionTimeSeconds);
    }
}

// ---------------------------------------------------------------------------
// Set recipe
// ---------------------------------------------------------------------------

void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
{
    // Construction site: store recipe for when building completes.
    for (ConstructionSite& site : m_state.constructionQueue)
    {
        if (site.id == id)
        {
            // Auto-recipe buildings have no player-selected recipe
            // (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
            if (isAutoRecipeBuildingType(site.type))
            {
                return;
            }
            // No-op if the recipe is unchanged, so a redundant selection does
            // not wipe an already-configured ship layout.
            if (site.recipeId == recipeId)
            {
                return;
            }
            site.recipeId = recipeId;
            site.shipLayout = std::nullopt;
            return;
        }
    }

    // Operational building: clear buffers and re-init.
    for (Building& building : m_state.buildings)
    {
        if (building.id == id)
        {
            // Auto-recipe buildings have no player-selected recipe
            // (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
            if (isAutoRecipeBuildingType(building.type))
            {
                return;
            }
            // No-op if the recipe is unchanged, so a redundant selection does
            // not wipe an already-configured ship layout or reset buffers.
            if (building.recipeId == recipeId)
            {
                return;
            }
            building.recipeId = recipeId;
            building.shipLayout = std::nullopt;
            building.inputBuffer.counts.clear();
            building.inputBuffer.caps.clear();
            building.outputBuffer.items.clear();
            building.outputBuffer.capacity = 0;
            // Emerging items are part of the output buffer, so clearing it on a
            // recipe change discards them too (REQ-MAT-OUTPUT-EMERGE); in-transit
            // input items are discarded and their reservations released
            // (REQ-MAT-INPUT-INTAKE).
            for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
            for (std::vector<BeltItemSlot>& lane : building.incomingItems) { lane.clear(); }
            building.production = std::nullopt;

            if (!recipeId.empty())
            {
                if (building.type == BuildingType::Shipyard)
                {
                    initShipyardBuffers(building);
                }
                else
                {
                    const RecipeDef* recipe = m_config.recipes.findRecipeDef(recipeId, building.type);
                    if (recipe)
                    {
                        initBuffers(building, *recipe);
                    }
                }
            }
            return;
        }
    }
}

void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout)
{
    for (ConstructionSite& site : m_state.constructionQueue)
    {
        if (site.id == id)
        {
            site.shipLayout = layout;
            return;
        }
    }

    for (Building& building : m_state.buildings)
    {
        if (building.id == id)
        {
            if (building.production.has_value())
            {
                building.production = std::nullopt;
            }
            building.shipLayout = layout;
            building.inputBuffer.counts.clear();
            building.inputBuffer.caps.clear();
            building.outputBuffer.items.clear();
            building.outputBuffer.capacity = 0;
            for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
            for (std::vector<BeltItemSlot>& lane : building.incomingItems) { lane.clear(); }
            if (!building.recipeId.empty() && building.type == BuildingType::Shipyard)
            {
                initShipyardBuffers(building);
            }
            return;
        }
    }
}

std::optional<BeltSystem::SplitterInfo>
BuildingSystem::getSiteSplitterInfo(BuildingId id) const
{
    for (const ConstructionSite& site : m_state.constructionQueue)
    {
        if (site.id != id) { continue; }
        if (site.type != BuildingType::Splitter) { return std::nullopt; }

        const BuildingDef* def = m_config.buildings.findBuildingDef(site.type);
        const ParsedSurfaceMask mask = parseSurfaceMask(
            def ? def->surfaceMask : std::vector<std::string>{}, site.rotation);
        if (mask.outputPorts.size() < 2) { return std::nullopt; }

        BeltSystem::SplitterInfo info;
        info.outputA = mask.outputPorts[0].direction;
        info.outputB = mask.outputPorts[1].direction;
        info.filterA = site.splitterFilterA;
        info.filterB = site.splitterFilterB;
        return info;
    }
    return std::nullopt;
}

void BuildingSystem::setSiteSplitterFilters(BuildingId id,
                                            const std::vector<ItemType>& filterA,
                                            const std::vector<ItemType>& filterB)
{
    for (ConstructionSite& site : m_state.constructionQueue)
    {
        if (site.id == id && site.type == BuildingType::Splitter)
        {
            site.splitterFilterA = filterA;
            site.splitterFilterB = filterB;
            return;
        }
    }
}

// ---------------------------------------------------------------------------
// Tick hooks
// ---------------------------------------------------------------------------

void BuildingSystem::tickConstruction(Tick currentTick)
{
	TRACE();
    if (m_state.constructionQueue.empty())
    {
        return;
    }

    ConstructionSite& front = m_state.constructionQueue.front();

    // Guard: if somehow the front site was never started, start it now.
    if (front.completesAt == 0)
    {
        const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
        if (def)
        {
            front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
        }
        return;
    }

    if (currentTick < front.completesAt)
    {
        return;
    }

    // Promote construction site to an operational Building.
    const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
    const ParsedSurfaceMask mask = parseSurfaceMask(
        def ? def->surfaceMask : std::vector<std::string>{},
        front.rotation);

    Building building;
    building.id        = front.id;
    building.anchor    = front.anchor;
    building.footprint = front.footprint;
    building.rotation  = front.rotation;
    building.type      = front.type;
    building.recipeId  = front.recipeId;
    building.shipLayout = front.shipLayout;

    for (const QPoint& cell : mask.bodyCells)
    {
        building.bodyCells.push_back(front.anchor + cell);
    }
    for (const Port& port : mask.outputPorts)
    {
        Port absPort;
        absPort.tile      = front.anchor + port.tile;
        absPort.direction = port.direction;
        building.outputPorts.push_back(absPort);
    }
    building.emergingItems.resize(building.outputPorts.size());
    building.inputPorts = computeInputPorts(building);
    building.incomingItems.assign(building.inputPorts.size(), {});

    if (building.type == BuildingType::SalvageBay)
    {
        initSalvageBayBuffer(building);
    }
    else if (isAutoRecipeBuildingType(building.type))
    {
        // Smelter/Reprocessing Plant need no recipe selection; buffers are set
        // up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
        initAutoBuffers(building);
    }
    else if (!building.recipeId.empty())
    {
        if (building.type == BuildingType::Shipyard)
        {
            initShipyardBuffers(building);
        }
        else
        {
            const RecipeDef* recipe = m_config.recipes.findRecipeDef(building.recipeId, building.type);
            if (recipe)
            {
                initBuffers(building, *recipe);
            }
        }
    }

    // Register with BeltSystem before the move (mask/building stays valid). Any
    // filters configured while under construction carry over (REQ-BLD-SITE-CONFIG).
    reregisterBeltTile(building, front.splitterFilterA, front.splitterFilterB);

    m_state.buildings.push_back(std::move(building));

    m_state.constructionQueue.pop_front();

    // Start next queued site if present.
    if (!m_state.constructionQueue.empty() && m_state.constructionQueue.front().completesAt == 0)
    {
        const BuildingDef* nextDef =
            m_config.buildings.findBuildingDef(m_state.constructionQueue.front().type);
        if (nextDef)
        {
            m_state.constructionQueue.front().completesAt =
                currentTick + secondsToTicks(nextDef->constructionTimeSeconds);
        }
    }
}

void BuildingSystem::reregisterBeltTile(const Building& building,
                                        const std::vector<ItemType>& splitterFilterA,
                                        const std::vector<ItemType>& splitterFilterB)
{
    switch (building.type)
    {
        case BuildingType::Belt:
            m_belts.placeBelt(building.anchor, building.rotation);
            break;
        case BuildingType::Splitter:
            assert(building.outputPorts.size() >= 2);
            m_belts.placeSplitter(building.anchor,
                                  building.outputPorts[0].direction,
                                  building.outputPorts[1].direction);
            m_belts.setSplitterFilters(building.anchor, splitterFilterA, splitterFilterB);
            break;
        case BuildingType::TunnelEntry:
            m_belts.placeTunnelEntry(building.anchor, building.rotation,
                                     m_config.world.tunnelMaxDistance_tiles);
            break;
        case BuildingType::TunnelExit:
            m_belts.placeTunnelExit(building.anchor, building.rotation);
            break;
        default:
            break;
    }
}

void BuildingSystem::tickDeconstruction(Tick currentTick)
{
    TRACE();
    if (m_state.deconstructionQueue.empty())
    {
        return;
    }

    DeconstructionEntry& front = m_state.deconstructionQueue.front();

    // Guard: if the front entry's timer was never started, start it now.
    if (front.completesAt == 0)
    {
        startFrontDeconstruction(currentTick);
        return;
    }

    if (currentTick < front.completesAt)
    {
        return;
    }

    // Remove the building from the world and credit its refund (REQ-BLD-DECONSTRUCT).
    // Belt/tunnel/splitter tiles were already unregistered when the building was
    // queued (see deconstruct), so only tile occupancy and the record remain.
    for (std::vector<Building>::iterator it = m_state.buildings.begin();
         it != m_state.buildings.end();
         ++it)
    {
        if (it->id != front.id) { continue; }

        const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
        m_state.grid.release(it->bodyCells);
        m_state.buildings.erase(it);
        if (def)
        {
            m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
        }
        break;
    }

    m_state.deconstructionQueue.pop_front();

    // Start the next queued deconstruction, if any.
    startFrontDeconstruction(currentTick);
}

void BuildingSystem::cancelDeconstruction(BuildingId id)
{
    for (std::deque<DeconstructionEntry>::iterator it = m_state.deconstructionQueue.begin();
         it != m_state.deconstructionQueue.end();
         ++it)
    {
        if (it->id != id) { continue; }

        // Resume operation: clear the flag and re-register belt/tunnel/splitter
        // tiles that were unregistered at enqueue (which re-pairs tunnels,
        // REQ-BLD-TUNNEL-PAIR). Deconstruction progress is discarded; no refund.
        if (Building* building = findBuildingMutable(id))
        {
            building->queuedForDeconstruction = false;
            reregisterBeltTile(*building, it->splitterFilterA, it->splitterFilterB);
        }

        m_state.deconstructionQueue.erase(it);
        // If the running front was removed, the new front (completesAt == 0) has
        // its timer started by the next tickDeconstruction guard.
        return;
    }
}

bool BuildingSystem::isQueuedForDeconstruction(BuildingId id) const
{
    const Building* building = findBuilding(id);
    return building && building->queuedForDeconstruction;
}

void BuildingSystem::tickBeltPull()
{
	TRACE();
    // Same per-tick step as the belts, so items travel inward at belt speed
    // (REQ-GW-BELT-SPEED, REQ-MAT-INPUT-INTAKE).
    const double progressPerTick = m_belts.getProgressPerTick_tpt();

    for (Building& building : m_state.buildings)
    {
        // A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
        if (building.queuedForDeconstruction) { continue; }

        const bool isHq = (building.type == BuildingType::Hq);

        // 1. Advance every input belt and deliver arrivals (progress >= 0.5) into
        //    the input buffer — or the global stock for the HQ. Runs for all
        //    buildings so in-transit items keep moving even when feeding is gated
        //    off, and arrivals become consumable before tickProduction (step 4).
        for (std::size_t i = 0; i < building.incomingItems.size(); ++i)
        {
            std::vector<BeltItemSlot>& lane = building.incomingItems[i];
            advanceBeltSlots(lane, progressPerTick);
            while (!lane.empty() && lane.front().progress >= 0.5)
            {
                const Item arrived = lane.front().item;
                lane.erase(lane.begin());
                if (isHq)
                {
                    m_addBuildingBlocks(1);
                }
                else
                {
                    building.inputBuffer.counts[arrived.type]++;
                }
            }
        }

        // 2. Feed accepted items from adjacent belts onto the input belts at
        //    progress 0.0. The acceptance rules — the HQ building-block case, the
        //    required-input check, and the reservation — live in canAcceptInput so
        //    direct coupling (REQ-MAT-DIRECT-COUPLE) shares them exactly.
        for (std::size_t i = 0; i < building.inputPorts.size(); ++i)
        {
            const std::optional<ItemType> peeked = m_belts.peekItem(building.inputPorts[i]);
            if (!peeked) { continue; }
            if (!canAcceptInput(building, i, *peeked)) { continue; }
            const std::optional<Item> taken = m_belts.tryTakeItem(building.inputPorts[i]);
            if (taken)
            {
                depositToInputBelt(building, i, *taken);
            }
        }
    }
}

bool BuildingSystem::canAcceptInput(const Building& consumer,
                                    std::size_t inputPortIndex,
                                    const ItemType& type) const
{
    if (inputPortIndex >= consumer.incomingItems.size()) { return false; }
    if (!inputLaneEntryFree(consumer.incomingItems[inputPortIndex])) { return false; }

    // The HQ has no input buffer; it accepts building blocks into the global stock
    // (REQ-HQ-BELT-INPUT) with no reservation.
    if (consumer.type == BuildingType::Hq)
    {
        return type.id == "building_block";
    }

    // Everyone else: the item must be a required input whose reservation-aware
    // buffer has room — buffered + in-transit below the cap (REQ-MAT-INPUT-INTAKE).
    const std::map<ItemType, int>::const_iterator capIt =
        consumer.inputBuffer.caps.find(type);
    if (capIt == consumer.inputBuffer.caps.end() || capIt->second == 0)
    {
        return false;
    }
    return consumer.pendingInputCount(type) < capIt->second;
}

void BuildingSystem::depositToInputBelt(Building& consumer,
                                        std::size_t inputPortIndex,
                                        const Item& item)
{
    consumer.incomingItems[inputPortIndex].push_back(BeltItemSlot{item, 0.0});
}

bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId,
                                            const Port& outputPort,
                                            const Item& item)
{
    const std::optional<BuildingId> ownerId = m_state.grid.findOwner(outputPort.tile);
    if (!ownerId.has_value() || *ownerId == producerId)
    {
        return false;
    }

    Building* consumer = findBuildingMutable(*ownerId);
    if (!consumer)
    {
        return false;  // an unbuilt construction site, or not an operational building
    }
    if (consumer->queuedForDeconstruction)
    {
        return false;  // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
    }

    // The coupling is the consumer input port meeting this output port: same flow
    // direction, feeding the producer's output-port tile (REQ-MAT-DIRECT-COUPLE).
    for (std::size_t j = 0; j < consumer->inputPorts.size(); ++j)
    {
        const Port& in = consumer->inputPorts[j];
        if (in.direction != outputPort.direction) { continue; }
        if (inputBodyTile(in.tile, in.direction) != outputPort.tile) { continue; }

        if (!canAcceptInput(*consumer, j, item.type)) { return false; }
        depositToInputBelt(*consumer, j, item);
        return true;
    }
    return false;
}

void BuildingSystem::tickProduction(Tick currentTick)
{
	TRACE();
    for (Building& building : m_state.buildings)
    {
        // A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
        if (building.queuedForDeconstruction) { continue; }

        // Skip types without a recipe-based production loop.
        if (building.type == BuildingType::Belt     ||
            building.type == BuildingType::Splitter  ||
            building.type == BuildingType::Shipyard  ||
            building.type == BuildingType::SalvageBay ||
            building.type == BuildingType::Hq)
        {
            continue;
        }

        const bool autoRecipe = isAutoRecipeBuildingType(building.type);
        if (!autoRecipe && building.recipeId.empty())
        {
            continue;
        }

        // If a production cycle is active, check for completion. Completion only
        // needs the already-decided outputs, so it does not depend on which
        // recipe is selected or auto-chosen.
        if (building.production)
        {
            if (currentTick >= building.production->completesAt)
            {
                for (const Item& item : building.production->chosenOutputs)
                {
                    building.outputBuffer.items.push_back(item);
                }
                building.production = std::nullopt;
            }
            // Whether we just completed or are still running, do not start
            // another cycle in the same tick.
            continue;
        }

        // Idle: gather the candidate recipes to try. Auto-recipe buildings
        // (Smelter, Reprocessing Plant) have no selected recipe and try every
        // recipe of their type in config order, running the first whose inputs
        // are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings
        // try only their selected recipe.
        const std::vector<const RecipeDef*> candidates =
            gatherCandidateRecipes(building);

        for (const RecipeDef* recipe : candidates)
        {
            // 1. All required inputs present?
            if (!recipeInputsAvailable(building, *recipe))
            {
                continue;
            }

            // 2. Determine chosen outputs (roll for reprocessing).
            std::vector<Item> chosen;
            if (building.type == BuildingType::ReprocessingPlant)
            {
                chosen = rollReprocessingOutput(*recipe);
                if (chosen.empty()) { continue; }
            }
            else
            {
                for (const RecipeOutput& out : recipe->outputs)
                {
                    Item item;
                    item.type.id = out.item;
                    for (int i = 0; i < out.amount; ++i)
                    {
                        chosen.push_back(item);
                    }
                }
            }

            // 3. Output buffer has space for chosen outputs? Emerging items still
            //    count against the buffer (REQ-MAT-OUTPUT-EMERGE).
            const int newSize = building.getOutputItemCount()
                              + static_cast<int>(chosen.size());
            if (newSize > building.outputBuffer.capacity)
            {
                continue;
            }

            // 4. Consume inputs and start cycle.
            for (const RecipeIngredient& ing : recipe->inputs)
            {
                building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
            }

            Production prod;
            prod.recipeId      = recipe->id;
            prod.completesAt   = currentTick + secondsToTicks(recipe->durationSeconds);
            prod.chosenOutputs = std::move(chosen);
            building.production = std::move(prod);
            break;  // At most one cycle starts per tick.
        }
    }
}

void BuildingSystem::tickShipyardProduction(Tick currentTick)
{
	TRACE();
    for (Building& building : m_state.buildings)
    {
        // A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
        if (building.queuedForDeconstruction) { continue; }

        if (building.type != BuildingType::Shipyard)
        {
            continue;
        }
        if (building.recipeId.empty())
        {
            continue;
        }
        const ShipDef* shipDef = m_config.ships.findShipDef(building.recipeId);
        if (!shipDef)
        {
            continue;
        }

        // If a cycle is in progress, check for completion.
        if (building.production)
        {
            if (currentTick >= building.production->completesAt)
            {
                if (!building.outputPorts.empty())
                {
                    const Port& p = building.outputPorts[0];
                    const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
                    // A shipyard builds exactly what the player configured and
                    // paid for. When no layout is set it produces a bare hull, so
                    // pass an explicit empty layout rather than nullopt: the latter
                    // would make ShipSystem fall back to the schematic's
                    // defaultModules (a wave-only loadout) and yield free weapons.
                    const std::optional<ShipLayoutConfig> layout =
                        building.shipLayout.has_value()
                            ? building.shipLayout
                            : std::make_optional<ShipLayoutConfig>();
                    m_spawnShip(building.recipeId, spawnPos, layout);
                }
                building.production = std::nullopt;
            }
            continue;
        }

        // Build combined materials list (base + modules).
        const std::map<std::string, int> requiredMaterials =
            computeShipyardRequiredMaterials(building);

        // Idle: check if all combined materials are available.
        bool inputsOk = true;
        for (const std::pair<const std::string, int>& req : requiredMaterials)
        {
            const ItemType type{req.first};
            const std::map<ItemType, int>::const_iterator it =
                building.inputBuffer.counts.find(type);
            const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
            if (have < req.second)
            {
                inputsOk = false;
                break;
            }
        }
        if (!inputsOk)
        {
            continue;
        }

        // Consume combined materials and start the production cycle.
        for (const std::pair<const std::string, int>& req : requiredMaterials)
        {
            building.inputBuffer.counts[ItemType{req.first}] -= req.second;
        }

        double totalTime = shipDef->schematic.productionTimeSeconds;
        if (building.shipLayout.has_value())
        {
            for (const PlacedModule& pm : building.shipLayout->placedModules)
            {
                const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
                if (modDef)
                {
                    totalTime += modDef->productionTimeSeconds;
                }
            }
        }

        Production prod;
        prod.recipeId    = building.recipeId;
        prod.completesAt = currentTick + secondsToTicks(totalTime);
        building.production = std::move(prod);
    }
}

void BuildingSystem::tickOutputBelts()
{
	TRACE();
    // Use BeltSystem's own per-tick step so emerging items travel at exactly the
    // same speed as real belts (REQ-GW-BELT-SPEED, REQ-MAT-OUTPUT-EMERGE).
    const double progressPerTick = m_belts.getProgressPerTick_tpt();

    for (Building& building : m_state.buildings)
    {
        // A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
        if (building.queuedForDeconstruction) { continue; }

        for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
        {
            const Port& port = building.outputPorts[p];
            std::vector<BeltItemSlot>& lane = building.emergingItems[p];

            // 1. Advance emerging items using the shared belt packing (progress
            //    caps to 0.5 / 0.75 / 1.0 for up to three items).
            advanceBeltSlots(lane, progressPerTick);

            // 2. Hand the front item off once it reaches the output edge (progress
            //    1.0): onto the adjacent real belt, or — if a building's input edge
            //    meets this port — straight into that building (REQ-MAT-DIRECT-COUPLE).
            //    On refusal (no belt/coupling, output-edge per REQ-MAT-ACCEPT-DIR, or
            //    a full target) it stays stuck at 1.0.
            if (!lane.empty() && lane.front().progress >= 1.0)
            {
                const Item item = lane.front().item;
                if (m_belts.tryPutItem(port.tile, item, port.direction)
                    || tryDirectCoupleDeposit(building.id, port, item))
                {
                    lane.erase(lane.begin());
                }
            }

            // 3. Feed the next buffered item onto the lane at progress 0.5 when the
            //    entry slot is free — the lane holds at most three items and a new
            //    one needs a quarter-tile clearance ahead of 0.5.
            if (!building.outputBuffer.items.empty()
                && lane.size() < 3
                && (lane.empty() || lane.back().progress >= 0.75))
            {
                lane.push_back(BeltItemSlot{building.outputBuffer.items.front(), 0.5});
                building.outputBuffer.items.erase(building.outputBuffer.items.begin());
            }
        }
    }
}

void BuildingSystem::forEachEmergingItem(
    const std::function<void(const ItemType&, QPointF)>& visit) const
{
    for (const Building& building : m_state.buildings)
    {
        for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
        {
            const Port& port = building.outputPorts[p];
            const QPoint bodyTile = outputBodyTile(port.tile, port.direction);
            const std::vector<BeltItemSlot>& lane = building.emergingItems[p];

            // Render least-progressed first (bottom) → most-progressed last (top),
            // matching belt item ordering (REQ-GW-TILE-SIZE).
            for (int i = static_cast<int>(lane.size()) - 1; i >= 0; --i)
            {
                visit(lane[i].item.type,
                      beltSlotWorldPos(bodyTile, port.direction, lane[i].progress));
            }
        }
    }
}

void BuildingSystem::forEachIncomingItem(
    const std::function<void(const ItemType&, QPointF)>& visit) const
{
    for (const Building& building : m_state.buildings)
    {
        for (std::size_t p = 0; p < building.inputPorts.size(); ++p)
        {
            const Port& port = building.inputPorts[p];
            const QPoint bodyTile = inputBodyTile(port.tile, port.direction);
            const std::vector<BeltItemSlot>& lane = building.incomingItems[p];

            // Render least-progressed first (bottom) → most-progressed last (top),
            // matching belt item ordering (REQ-GW-TILE-SIZE).
            for (int i = static_cast<int>(lane.size()) - 1; i >= 0; --i)
            {
                visit(lane[i].item.type,
                      beltSlotWorldPos(bodyTile, port.direction, lane[i].progress));
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------

const Building* BuildingSystem::findBuilding(BuildingId id) const
{
    for (const Building& building : m_state.buildings)
    {
        if (building.id == id)
        {
            return &building;
        }
    }
    return nullptr;
}

Building* BuildingSystem::findBuildingMutable(BuildingId id)
{
    for (Building& building : m_state.buildings)
    {
        if (building.id == id)
        {
            return &building;
        }
    }
    return nullptr;
}

const ConstructionSite* BuildingSystem::findSite(BuildingId id) const
{
    for (const ConstructionSite& site : m_state.constructionQueue)
    {
        if (site.id == id)
        {
            return &site;
        }
    }
    return nullptr;
}

std::vector<Building> BuildingSystem::getAllBuildings() const
{
    return m_state.buildings;
}

std::vector<ConstructionSite> BuildingSystem::getAllSites() const
{
    return std::vector<ConstructionSite>(m_state.constructionQueue.begin(),
                                         m_state.constructionQueue.end());
}

namespace
{
bool isProductionBuildingType(BuildingType type)
{
    switch (type)
    {
    case BuildingType::Miner:
    case BuildingType::Smelter:
    case BuildingType::Assembler:
    case BuildingType::ReprocessingPlant:
    case BuildingType::Shipyard:
        return true;
    default:
        return false;
    }
}
}  // namespace

int BuildingSystem::getProductionBuildingCount() const
{
    int count = 0;
    for (const Building& b : m_state.buildings)
    {
        if (isProductionBuildingType(b.type)) { ++count; }
    }
    return count;
}

int BuildingSystem::getActiveProductionBuildingCount() const
{
    int count = 0;
    for (const Building& b : m_state.buildings)
    {
        if (isProductionBuildingType(b.type) && b.production.has_value()) { ++count; }
    }
    return count;
}

std::vector<const RecipeDef*>
BuildingSystem::gatherCandidateRecipes(const Building& b) const
{
    std::vector<const RecipeDef*> candidates;
    if (isAutoRecipeBuildingType(b.type))
    {
        for (const RecipeDef& r : m_config.recipes.recipes)
        {
            if (r.building == b.type && !r.inputs.empty())
            {
                candidates.push_back(&r);
            }
        }
    }
    else
    {
        const RecipeDef* recipe = m_config.recipes.findRecipeDef(b.recipeId, b.type);
        if (recipe)
        {
            candidates.push_back(recipe);
        }
    }
    return candidates;
}

bool BuildingSystem::recipeInputsAvailable(const Building& b,
                                           const RecipeDef& recipe) const
{
    for (const RecipeIngredient& ing : recipe.inputs)
    {
        const std::map<ItemType, int>::const_iterator it =
            b.inputBuffer.counts.find(ItemType{ing.item});
        const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
        if (have < ing.amount)
        {
            return false;
        }
    }
    return true;
}

std::map<std::string, int>
BuildingSystem::computeShipyardRequiredMaterials(const Building& b) const
{
    std::map<std::string, int> requiredMaterials;
    const ShipDef* shipDef = m_config.ships.findShipDef(b.recipeId);
    if (!shipDef)
    {
        return requiredMaterials;
    }
    for (const RecipeIngredient& ing : shipDef->schematic.materials)
    {
        requiredMaterials[ing.item] += ing.amount;
    }
    if (b.shipLayout.has_value())
    {
        for (const PlacedModule& pm : b.shipLayout->placedModules)
        {
            const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
            if (!modDef)
            {
                continue;
            }
            for (const RecipeIngredient& ing : modDef->materials)
            {
                requiredMaterials[ing.item] += ing.amount;
            }
        }
    }
    return requiredMaterials;
}

bool BuildingSystem::hasInputsToStart(const Building& b) const
{
    if (b.type == BuildingType::Shipyard)
    {
        const std::map<std::string, int> required =
            computeShipyardRequiredMaterials(b);
        for (const std::pair<const std::string, int>& req : required)
        {
            const std::map<ItemType, int>::const_iterator it =
                b.inputBuffer.counts.find(ItemType{req.first});
            const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
            if (have < req.second)
            {
                return false;
            }
        }
        return true;
    }

    // Recipe buildings: startable if any candidate recipe's inputs are satisfied.
    // A Miner recipe has no inputs, so an idle Miner is always startable and its
    // only idle reason is a full output buffer.
    for (const RecipeDef* recipe : gatherCandidateRecipes(b))
    {
        if (recipeInputsAvailable(b, *recipe))
        {
            return true;
        }
    }
    return false;
}

std::optional<ProductionStatus>
BuildingSystem::getProductionStatus(const Building& building) const
{
    // Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is
    // "producing" while it holds scrap to push out, and starved when empty.
    if (building.type == BuildingType::SalvageBay)
    {
        return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing
                                                  : ProductionStatus::Starved;
    }

    // Only the five recipe/cycle production types show a status light besides the
    // Salvage Bay; belts, splitters, tunnels, HQ, and stations show none.
    if (!isProductionBuildingType(building.type))
    {
        return std::nullopt;
    }

    // Grey only applies to player-configured types; auto-recipe buildings
    // (Smelter, Reprocessing Plant) always run an implicit recipe.
    if (!isAutoRecipeBuildingType(building.type) && building.recipeId.empty())
    {
        return ProductionStatus::Unconfigured;
    }

    if (building.production.has_value())
    {
        return ProductionStatus::Producing;
    }

    // Idle: missing inputs (red) take precedence over a full output buffer
    // (yellow). If inputs are present yet the building is idle, the only remaining
    // reason it could not start a cycle is a full output buffer (REQ-MAT-CYCLE).
    return hasInputsToStart(building) ? ProductionStatus::Blocked
                                      : ProductionStatus::Starved;
}

std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::getAllBeltTiles() const
{
    std::vector<BeltTileInfo> result;
    for (const Building& b : m_state.buildings)
    {
        if (b.type != BuildingType::Belt && b.type != BuildingType::Splitter)
        {
            continue;
        }
        BeltTileInfo info;
        info.buildingId = b.id;
        info.tile = b.bodyCells.empty() ? b.anchor : b.bodyCells[0];
        info.type = b.type;
        if (!b.outputPorts.empty())
        {
            info.directionA = b.outputPorts[0].direction;
            info.directionB = b.outputPorts[0].direction;
        }
        else
        {
            info.directionA = b.rotation;
            info.directionB = b.rotation;
        }
        if (b.type == BuildingType::Splitter && b.outputPorts.size() >= 2)
        {
            info.directionB = b.outputPorts[1].direction;
        }
        result.push_back(info);
    }
    return result;
}

bool BuildingSystem::isTileOccupied(QPoint tile) const
{
    return m_state.grid.isOccupied(tile);
}

std::optional<BuildingId> BuildingSystem::findRotateInPlaceTarget(
    BuildingType type, QPoint anchor, Rotation rot) const
{
    // Tunnel Entries and Tunnel Exits cannot be rotated in place; re-orienting a
    // tunnel requires deconstructing and re-placing it (REQ-BLD-ROTATE-IN-PLACE).
    if (type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit)
    {
        return std::nullopt;
    }

    const BuildingDef* def = m_config.buildings.findBuildingDef(type);
    if (!def) { return std::nullopt; }

    const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rot);
    if (mask.bodyCells.empty()) { return std::nullopt; }

    // All body cells must be occupied by the same entity.
    const QPoint firstAbs = anchor + mask.bodyCells[0];
    const std::optional<BuildingId> firstOwner = m_state.grid.findOwner(firstAbs);
    if (!firstOwner.has_value()) { return std::nullopt; }
    const BuildingId candidateId = *firstOwner;

    for (const QPoint& rel : mask.bodyCells)
    {
        const std::optional<BuildingId> owner = m_state.grid.findOwner(anchor + rel);
        if (!owner.has_value() || *owner != candidateId)
        {
            return std::nullopt;
        }
    }

    // Verify the candidate is the same building type with the same cell count.
    for (const ConstructionSite& site : m_state.constructionQueue)
    {
        if (site.id != candidateId) { continue; }
        if (site.type != type) { return std::nullopt; }
        if (site.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
        return candidateId;
    }
    for (const Building& b : m_state.buildings)
    {
        if (b.id != candidateId) { continue; }
        if (b.type != type) { return std::nullopt; }
        if (b.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
        return candidateId;
    }

    return std::nullopt;
}

void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
{
    // Construction site path — just update rotation; no ports to recompute.
    for (ConstructionSite& site : m_state.constructionQueue)
    {
        if (site.id == id)
        {
            site.rotation = newRotation;
            return;
        }
    }

    // Operational building path.
    for (Building& b : m_state.buildings)
    {
        if (b.id != id) { continue; }

        b.rotation = newRotation;

        const BuildingDef* def = m_config.buildings.findBuildingDef(b.type);
        if (!def) { return; }
        const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, newRotation);

        b.outputPorts.clear();
        for (const Port& port : mask.outputPorts)
        {
            Port absPort;
            absPort.tile      = b.anchor + port.tile;
            absPort.direction = port.direction;
            b.outputPorts.push_back(absPort);
        }
        // The output ports moved; discard any in-flight emerging items and re-size
        // the lanes to the new port set (REQ-MAT-OUTPUT-EMERGE).
        b.emergingItems.clear();
        b.emergingItems.resize(b.outputPorts.size());
        b.inputPorts = computeInputPorts(b);
        // Likewise discard in-transit input items and re-size the input belts to
        // the new port set (REQ-MAT-INPUT-INTAKE).
        b.incomingItems.assign(b.inputPorts.size(), {});

        // Re-register with BeltSystem (items on tile are discarded). A splitter's
        // filters live in BeltSystem and would be lost by removeTile, so capture
        // them first and hand them back to reregisterBeltTile (REQ-BLD-SPLITTER).
        if (isBeltSubsystemType(b.type))
        {
            std::vector<ItemType> splitterFilterA;
            std::vector<ItemType> splitterFilterB;
            if (b.type == BuildingType::Splitter)
            {
                if (const std::optional<BeltSystem::SplitterInfo> info =
                        m_belts.getSplitterInfo(b.anchor))
                {
                    splitterFilterA = info->filterA;
                    splitterFilterB = info->filterB;
                }
            }

            m_belts.removeTile(b.anchor);
            reregisterBeltTile(b, splitterFilterA, splitterFilterB);
        }

        return;
    }
}

const Building* BuildingSystem::findNearestBuilding(QVector2D worldPos,
                                                    BuildingType type) const
{
    const Building* best = nullptr;
    float bestDist = std::numeric_limits<float>::max();
    for (const Building& b : m_state.buildings)
    {
        if (b.type != type)
        {
            continue;
        }
        QVector2D center(b.anchor.x() + b.footprint.width()  / 2.0f,
                         b.anchor.y() + b.footprint.height() / 2.0f);
        float dist = (center - worldPos).length();
        if (dist < bestDist)
        {
            bestDist = dist;
            best = &b;
        }
    }
    return best;
}

bool BuildingSystem::deliverScrapToSalvageBay(BuildingId bayId)
{
    Building* bay = nullptr;
    for (Building& b : m_state.buildings)
    {
        if (b.id == bayId)
        {
            bay = &b;
            break;
        }
    }
    if (!bay || bay->type != BuildingType::SalvageBay)
    {
        return false;
    }
    if (bay->queuedForDeconstruction)
    {
        return false;  // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
    }
    // Emerging scrap still counts against the bay's holding capacity
    // (REQ-MAT-OUTPUT-EMERGE).
    if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
    {
        return false;
    }
    bay->outputBuffer.items.push_back(Item{ItemType{"scrap"}});
    return true;
}

BuildingId BuildingSystem::placeImmediate(BuildingType type,
                                         const std::vector<std::string>& surfaceMask,
                                         QPoint anchor, Rotation rotation)
{
    const BuildingId id = m_allocateBuildingId();
    const ParsedSurfaceMask mask = parseSurfaceMask(surfaceMask, rotation);

    Building building;
    building.id        = id;
    building.anchor    = anchor;
    building.footprint = mask.footprint;
    building.rotation  = rotation;
    building.type      = type;

    for (const QPoint& cell : mask.bodyCells)
    {
        const QPoint absCell = anchor + cell;
        building.bodyCells.push_back(absCell);
        m_state.grid.occupy(absCell, id);
    }
    for (const Port& port : mask.outputPorts)
    {
        Port absPort;
        absPort.tile      = anchor + port.tile;
        absPort.direction = port.direction;
        building.outputPorts.push_back(absPort);
    }
    building.emergingItems.resize(building.outputPorts.size());
    building.inputPorts = computeInputPorts(building);
    building.incomingItems.assign(building.inputPorts.size(), {});

    if (type == BuildingType::SalvageBay)
    {
        initSalvageBayBuffer(building);
    }

    m_state.buildings.push_back(std::move(building));
    return id;
}

bool BuildingSystem::removeBuilding(BuildingId id)
{
    for (std::vector<Building>::iterator it = m_state.buildings.begin();
         it != m_state.buildings.end();
         ++it)
    {
        if (it->id == id)
        {
            if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
                || it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
            {
                m_belts.removeTile(it->anchor);
            }
            m_state.grid.release(it->bodyCells);
            m_state.buildings.erase(it);
            return true;
        }
    }
    return false;
}

void BuildingSystem::forEachBuilding(std::function<void(Building&)> fn)
{
    for (Building& b : m_state.buildings)
    {
        fn(b);
    }
}

void BuildingSystem::registerTileOccupancy(const std::vector<QPoint>& cells,
                                            BuildingId ownerPlaceholder)
{
    m_state.grid.occupy(cells, ownerPlaceholder);
}

void BuildingSystem::unregisterTileOccupancy(const std::vector<QPoint>& cells)
{
    m_state.grid.release(cells);
}

namespace
{
void appendItems(Hasher& hasher, const std::vector<Item>& items)
{
    hasher.append(items.size());
    for (const Item& item : items)
    {
        hasher.append(item.type.id);
    }
}

void appendInputBuffer(Hasher& hasher, const InputBuffer& buffer)
{
    // std::map<ItemType, int> iterates in sorted-id order (ItemType::operator<).
    hasher.append(buffer.counts.size());
    for (const std::pair<const ItemType, int>& entry : buffer.counts)
    {
        hasher.append(entry.first.id);
        hasher.append(entry.second);
    }
    hasher.append(buffer.caps.size());
    for (const std::pair<const ItemType, int>& entry : buffer.caps)
    {
        hasher.append(entry.first.id);
        hasher.append(entry.second);
    }
}
}  // namespace

void BuildingSystem::appendChecksum(Hasher& hasher) const
{
    // m_state.buildings keeps a stable, deterministic order (append on build, swap-free
    // erase aside — both runs perform identical operations, so order matches).
    hasher.append(m_state.buildings.size());
    for (const Building& b : m_state.buildings)
    {
        hasher.append(b.id);
        hasher.append(b.anchor);
        hasher.append(b.footprint.width());
        hasher.append(b.footprint.height());
        hasher.append(b.rotation);
        hasher.append(b.type);
        hasher.append(b.recipeId);
        appendInputBuffer(hasher, b.inputBuffer);
        appendItems(hasher, b.outputBuffer.items);
        hasher.append(b.outputBuffer.capacity);
        hasher.append(b.emergingItems.size());
        for (const std::vector<BeltItemSlot>& lane : b.emergingItems)
        {
            hasher.append(lane.size());
            for (const BeltItemSlot& slot : lane)
            {
                hasher.append(slot.item.type.id);
                hasher.append(slot.progress);
            }
        }
        hasher.append(b.incomingItems.size());
        for (const std::vector<BeltItemSlot>& lane : b.incomingItems)
        {
            hasher.append(lane.size());
            for (const BeltItemSlot& slot : lane)
            {
                hasher.append(slot.item.type.id);
                hasher.append(slot.progress);
            }
        }
        hasher.append(b.production.has_value());
        if (b.production.has_value())
        {
            hasher.append(b.production->recipeId);
            hasher.append(b.production->completesAt);
            appendItems(hasher, b.production->chosenOutputs);
        }
        hasher.append(b.shipLayout.has_value());
        hasher.append(b.queuedForDeconstruction);
    }

    hasher.append(m_state.constructionQueue.size());
    for (const ConstructionSite& s : m_state.constructionQueue)
    {
        hasher.append(s.id);
        hasher.append(s.anchor);
        hasher.append(s.footprint.width());
        hasher.append(s.footprint.height());
        hasher.append(s.rotation);
        hasher.append(s.type);
        hasher.append(s.recipeId);
        hasher.append(s.completesAt);
        hasher.append(s.shipLayout.has_value());
        hasher.append(s.splitterFilterA.size());
        for (const ItemType& type : s.splitterFilterA) { hasher.append(type.id); }
        hasher.append(s.splitterFilterB.size());
        for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); }
    }

    hasher.append(m_state.deconstructionQueue.size());
    for (const DeconstructionEntry& e : m_state.deconstructionQueue)
    {
        hasher.append(e.id);
        hasher.append(e.completesAt);
        hasher.append(e.splitterFilterA.size());
        for (const ItemType& type : e.splitterFilterA) { hasher.append(type.id); }
        hasher.append(e.splitterFilterB.size());
        for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); }
    }

    m_state.grid.appendChecksum(hasher);
}
