#include "ProductionSystem.h"

#include <algorithm>
#include <optional>
#include <random>
#include <vector>

#include "BeltSystem.h"
#include "BuildingBuffers.h"
#include "FactoryQueries.h"
#include "PortGeometry.h"
#include "ProductionRules.h"
#include "tracing.h"

ProductionSystem::ProductionSystem(const GameConfig& config,
                                   std::function<void(const std::string&, QVector2D,
                                                    const std::optional<ShipLayoutConfig>&)> spawnShip,
                                   std::function<bool(const std::string&)> isItemUnlocked,
                                   std::function<bool(const std::string&)> isRecipeUnlocked,
                                   std::mt19937& rng)
    : m_config(config)
    , m_spawnShip(std::move(spawnShip))
    , m_isItemUnlocked(std::move(isItemUnlocked))
    , m_isRecipeUnlocked(std::move(isRecipeUnlocked))
    , m_rng(rng)
{
}

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);
}

// The items of one group, produced together (REQ-MAT-OUTPUT-GROUP).
std::vector<Item> itemsOf(const RecipeOutputGroup& group)
{
    std::vector<Item> result;
    for (const RecipeOutput& out : group.items)
    {
        Item item;
        item.type.id = out.item;
        for (int i = 0; i < out.amount; ++i)
        {
            result.push_back(item);
        }
    }
    return result;
}
}  // namespace


std::vector<Item> ProductionSystem::rollOutputGroup(const RecipeDef& recipe)
{
    // One group: nothing to choose, so no weight is read, no draw is made, and no
    // eligibility is tested (REQ-MAT-OUTPUT-GROUP, REQ-LOCK-OUTPUT-POOL).
    //
    // Not drawing matters beyond speed. A draw here would consume entropy for every
    // ordinary recipe, shifting every later random outcome and invalidating recorded
    // replays. And eligibility must not apply either: implicit unlocking is derived from
    // demand, so an ordinary recipe's output can be perfectly producible while nothing
    // yet calls for it -- testing it here would stop the building producing at all.
    if (recipe.outputGroups.size() == 1)
    {
        return itemsOf(recipe.outputGroups.front());
    }

    // Several groups: only the unlocked ones can be picked (REQ-LOCK-OUTPUT-POOL), and
    // weights are renormalized over what is left by discrete_distribution.
    std::vector<const RecipeOutputGroup*> eligible;
    std::vector<double> weights;
    for (const RecipeOutputGroup& group : recipe.outputGroups)
    {
        if (!isOutputGroupUnlocked(group, m_isItemUnlocked)) { continue; }
        eligible.push_back(&group);
        weights.push_back(group.probability.value_or(1.0));
    }

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

    std::discrete_distribution<int> dist(weights.begin(), weights.end());
    return itemsOf(*eligible[static_cast<std::size_t>(dist(m_rng))]);
}

void ProductionSystem::tickBeltPull(FactoryState& state, BeltSystem& belts)
{
	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 = belts.getProgressPerTick_tpt();

    for (Building& building : 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)
                {
                    state.buildingBlocksStock += 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 = belts.peekItem(building.inputPorts[i]);
            if (!peeked) { continue; }
            // A Smelter or Reprocessing Plant without a recipe takes the first material
            // offered to it as its selection (REQ-BLD-AUTO-RECIPE); the ports are walked
            // in order, so which offer comes first is fixed.
            selectAutoRecipeIfUnset(building, *peeked);
            if (!canAcceptInput(building, i, *peeked)) { continue; }
            const std::optional<Item> taken = belts.tryTakeItem(building.inputPorts[i]);
            if (taken)
            {
                depositToInputBelt(building, i, *taken);
            }
        }
    }
}

void ProductionSystem::selectAutoRecipeIfUnset(Building& building, const ItemType& offered)
{
    // Only while it holds none: once set, a recipe is the player's to change
    // (REQ-BLD-AUTO-RECIPE). Buildings that select their own recipe are the only ones
    // this applies to; everyone else ignores an offer they have no recipe for.
    if (!building.recipeId.empty())
    {
        return;
    }
    const RecipeDef* recipe = findAutoRecipeFor(m_config, building.type, offered);
    if (!recipe)
    {
        return;
    }
    // Only a recipe the player could have selected themselves (REQ-LOCK-UI-RECIPE): a
    // building must not drift into running one that can yield them nothing. Refusing it
    // leaves the building unconfigured and so without an input buffer, which is what
    // keeps the offered item on the belt rather than swallowing it into a building that
    // has no use for it (REQ-MAT-INPUT-INTAKE).
    if (!m_isRecipeUnlocked(recipe->id))
    {
        return;
    }

    building.recipeId = recipe->id;
    initBuffers(building, *recipe);
}

bool ProductionSystem::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 ProductionSystem::depositToInputBelt(Building& consumer,
                                        std::size_t inputPortIndex,
                                        const Item& item)
{
    consumer.incomingItems[inputPortIndex].push_back(BeltItemSlot{item, 0.0});
}

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

    Building* consumer = findBuilding(state, *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; }

        // A coupling is an offer too, so an unset auto-recipe building selects from it
        // (REQ-BLD-AUTO-RECIPE). Without this a Smelter placed flush against a producer
        // would accept nothing and leave it stuck at its port for good.
        selectAutoRecipeIfUnset(*consumer, item.type);
        if (!canAcceptInput(*consumer, j, item.type)) { return false; }
        depositToInputBelt(*consumer, j, item);
        return true;
    }
    return false;
}

void ProductionSystem::tickProduction(FactoryState& state, Tick currentTick)
{
	TRACE();
    for (Building& building : 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;
        }

        if (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.
        if (building.production)
        {
            if (currentTick < building.production->completesAt)
            {
                continue;
            }
            for (const Item& item : building.production->chosenOutputs)
            {
                building.outputBuffer.items.push_back(item);
            }
            building.production = std::nullopt;
            // Fall through to the start attempt below rather than idling for a tick,
            // so a cycle takes exactly its recipe duration and a building fed to
            // capacity produces at the configured rate (REQ-MAT-CYCLE). The start
            // code runs once per building per tick, so at most one cycle begins here
            // even when a duration rounds to zero ticks. The outputs just deposited
            // count against the space check, so a cycle whose output no longer fits
            // waits, exactly as it would have on the following tick.
        }

        // Idle: try to start the building's one selected recipe. Every type holds
        // exactly one, a Smelter and a Reprocessing Plant included -- they differ only
        // in how theirs first got set (REQ-BLD-AUTO-RECIPE).
        const RecipeDef* recipe = getSelectedRecipe(m_config, building);
        if (!recipe)
        {
            continue;
        }

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

        // 2. Room for every output this cycle could produce -- checked before anything
        //    is rolled (REQ-MAT-CYCLE). The roll below is committed the moment the cycle
        //    starts, so a plant that could not store some outcome must not start at all:
        //    that is what stops a stalled output belt from biasing the distribution
        //    towards the outputs that still fit. Emerging items count against their
        //    buffer (REQ-MAT-OUTPUT-EMERGE). The status light asks the same question to
        //    decide yellow (REQ-UI-STATUS-LIGHT), so the test lives in one place.
        if (!recipeOutputsFit(building, *recipe))
        {
            continue;
        }

        // 3. Settle what this cycle produces: its one output group, picked by weight only
        //    where the recipe has several (REQ-MAT-OUTPUT-GROUP). Empty means every group
        //    was ineligible, so there is nothing to run.
        std::vector<Item> chosen = rollOutputGroup(*recipe);
        if (chosen.empty()) { 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);
    }
}

void ProductionSystem::tickShipyardProduction(FactoryState& state, Tick currentTick)
{
	TRACE();
    for (Building& building : 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)
            {
                continue;
            }
            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;
            // Fall through and start the next cycle in this same tick, so a ship takes
            // exactly its computed production time (REQ-BLD-SHIPYARD), as for the
            // recipe buildings in tickProduction.
        }

        // Build combined materials list (base + modules).
        const std::map<std::string, int> requiredMaterials =
            computeShipyardRequiredMaterials(m_config, 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 ProductionSystem::tickOutputBelts(FactoryState& state, BeltSystem& belts)
{
	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 = belts.getProgressPerTick_tpt();

    for (Building& building : 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 (belts.tryPutItem(port.tile, item, port.direction)
                    || tryDirectCoupleDeposit(state, 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());
            }
        }
    }
}
