#pragma once

#include <deque>
#include <functional>
#include <map>
#include <optional>
#include <random>
#include <string>
#include <utility>
#include <vector>

#include <QPoint>
#include <QPointF>
#include <QVector2D>

#include "BeltSystem.h"
#include "Building.h"
#include "FactoryState.h"
#include "BuildingType.h"
#include "BuildingId.h"
#include "GameConfig.h"
#include "Rotation.h"
#include "ModulesConfig.h"
#include "ShipLayout.h"
#include "ShipsConfig.h"
#include "Tick.h"

class Hasher;

// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT).
// The simulation owns the classification so it stays in sync with the
// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill
// color.
enum class ProductionStatus
{
    Unconfigured,  // no recipe/schematic selected (grey)
    Producing,     // a production cycle is active (green)
    Starved,       // idle: a required input is missing / Salvage Bay empty (red)
    Blocked,       // idle: output buffer full, inputs otherwise present (yellow)
};

// Manages building placement, construction queuing, and the per-tick
// production loop (belt→building pull, production, building→belt push).
// All types including Belt and Splitter are stored as Building instances;
// BeltSystem owns the per-tile simulation data (item slots, flow).
class BuildingSystem
{
public:
    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);

    // -- Placement / deconstruct ------------------------------------------------
    // Returns the new entity id, or nullopt if the placement falls outside the
    // world bounds (vertical extent and asteroid left edge).  Belt and Splitter
    // register with BeltSystem directly; other types enter the construction
    // queue.  Terrain type (A vs S) is NOT checked here so that tests can stage
    // arbitrary layouts; the player-facing entry point
    // (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
    std::optional<BuildingId> place(BuildingType type, QPoint anchor, Rotation rotation,
                   Tick currentTick);

    // Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and
    // world-bounds rules: every ship-dock (S) cell sits in space (x >= 0), every
    // other body (A) cell sits on the asteroid (x < 0 and x >= the left edge),
    // and every cell has 0 <= y < world.height_tiles. There is no right-side
    // bound — space extends rightward. Tile occupancy is NOT checked here.
    bool isPlacementValid(BuildingType type, QPoint anchor,
                          Rotation rotation) const;

    // Sets the current buildable asteroid width in tiles. Grows the left
    // placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
    // Defaults to world.regions.asteroid_width_tiles at construction.
    void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }

    // Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
    // A construction site is removed instantly and the full cost is returned.
    // A fully-built building is instead appended to the deconstruction queue
    // (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
    // credited later, on completion in tickDeconstruction, so this returns 0 for
    // it. Returns 0 for unknown ids and for a building already queued.
    int deconstruct(BuildingId id, Tick currentTick);

    // Take a building back out of the deconstruction queue before it is removed
    // (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
    // (re-registering belt/tunnel/splitter tiles); discards deconstruction
    // progress and credits no refund. No-op if the id is not queued.
    void cancelDeconstruction(BuildingId id);

    // True if the building is currently in the deconstruction queue.
    bool isQueuedForDeconstruction(BuildingId id) const;

    // Set the recipe (or schematic id for shipyard) on a building or queued
    // construction site.  Clears both buffers on an operational building.
    void setRecipe(BuildingId id, const std::string& recipeId);

    // Set the module layout for a shipyard. Cancels in-progress production
    // (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
    void setShipLayout(BuildingId id, const ShipLayoutConfig& layout);

    // Splitter filter configuration for a queued/under-construction Splitter
    // site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
    // BeltSystem by tile; these mirror that for sites, which are not yet
    // registered with BeltSystem. getSiteSplitterInfo returns the site's two
    // output directions (derived from its surface mask) and stored filters, or
    // nullopt if the id is not a Splitter site. The stored filters are applied
    // to BeltSystem when the splitter finishes building (tickConstruction).
    std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(BuildingId id) const;
    void setSiteSplitterFilters(BuildingId id,
                                const std::vector<ItemType>& filterA,
                                const std::vector<ItemType>& filterB);

    // -- Tick hooks (called from Simulation::tick in the documented order) ---
    void tickConstruction(Tick currentTick);
    // Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
    // time, in parallel with tickConstruction. Removes the front building and
    // credits its refund when its timer elapses.
    void tickDeconstruction(Tick currentTick);
    void tickBeltPull();
    void tickProduction(Tick currentTick);
    void tickShipyardProduction(Tick currentTick);
    // Advances each building's virtual output belts, hands finished items off onto
    // the adjacent real belt, and feeds new buffered items into them
    // (REQ-MAT-OUTPUT-EMERGE).
    void tickOutputBelts();

    // -- Queries -------------------------------------------------------------
    struct BeltTileInfo
    {
        BuildingId     buildingId;
        QPoint       tile;
        BuildingType type;       // Belt or Splitter
        Rotation     directionA; // Belt: its direction; Splitter: first output
        Rotation     directionB; // Splitter: second output; Belt: same as directionA
    };

    const Building*           findBuilding(BuildingId id)  const;
    const ConstructionSite*   findSite(BuildingId id)      const;
    std::vector<Building>     getAllBuildings()             const;
    std::vector<ConstructionSite> getAllSites()             const;

    // REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
    // (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
    int getProductionBuildingCount() const;

    // REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
    // that currently has an active production cycle.
    int getActiveProductionBuildingCount() const;

    // Production state for the UI status light (REQ-UI-STATUS-LIGHT). Returns
    // nullopt for building types that show no light (belts, splitters, tunnels,
    // HQ, defence stations). The Salvage Bay is a two-state special case:
    // Producing while its output buffer holds scrap, Starved when empty.
    std::optional<ProductionStatus> getProductionStatus(const Building& building) const;
    std::vector<BeltTileInfo> getAllBeltTiles()             const;
    bool                      isTileOccupied(QPoint tile) const;

    // Visits every item currently emerging from a building output port on its
    // virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its
    // world-space centre (in tile units). Least-progressed first (drawn bottom) so
    // callers can paint in visit order (REQ-GW-TILE-SIZE ordering).
    void forEachEmergingItem(
        const std::function<void(const ItemType&, QPointF)>& visit) const;

    // Visits every item currently travelling inward on a building input port's
    // virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its
    // world-space centre (in tile units). Least-progressed first (drawn bottom).
    void forEachIncomingItem(
        const std::function<void(const ItemType&, QPointF)>& visit) const;

    // Returns the entity id of the building or construction site whose footprint
    // exactly coincides with the ghost (type, anchor, rot) and is of the same
    // building type.  Returns nullopt otherwise.
    std::optional<BuildingId> findRotateInPlaceTarget(BuildingType type,
                                                     QPoint anchor,
                                                     Rotation rot) const;

    // Rotate an existing building or construction site to newRotation in place.
    // For belt-type operational buildings, re-registers with BeltSystem (items
    // currently on the tile are discarded by BeltSystem::removeTile).
    void rotateInPlace(BuildingId id, Rotation newRotation);

    // Find nearest operational building of the given type; nullptr if none.
    const Building* findNearestBuilding(QVector2D worldPos, BuildingType type) const;

    // Input-capable adjacent tiles for a building or construction site
    // (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS): each returned Port.tile is the
    // outside adjacent tile and Port.direction is the belt facing that points into
    // the target. Output-port edges are excluded. Empty for an unknown id.
    std::vector<Port> getInputPorts(BuildingId id) const;

    // Register / unregister tile occupancy for ECS station entities.
    void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
    void unregisterTileOccupancy(const std::vector<QPoint>& cells);

    // Place one "scrap" item into a SalvageBay's output buffer.
    // Returns false if bay not found, wrong type, or output buffer is full.
    bool deliverScrapToSalvageBay(BuildingId bayId);

    // Bypass the construction queue and create a fully-operational Building
    // immediately. Used for pre-placed structures (HQ, defence stations).
    // surfaceMask comes from the relevant config struct.
    BuildingId placeImmediate(BuildingType type,
                            const std::vector<std::string>& surfaceMask,
                            QPoint anchor, Rotation rotation);

    // Remove an operational building by id without refund (used for deaths).
    // Returns true if found and removed.
    bool removeBuilding(BuildingId id);

    // Mutable iteration over all operational buildings.
    void forEachBuilding(std::function<void(Building&)> fn);

    // -- Determinism ---------------------------------------------------------
    // Folds all building, construction-site, and tile-occupancy state into the
    // hasher in deterministic order (see docs/replay_design.md).
    void appendChecksum(Hasher& hasher) const;

private:
    // Starts the front deconstruction-queue entry's timer if not yet started
    // (mirrors how tickConstruction starts a queued construction site).
    void                 startFrontDeconstruction(Tick currentTick);

    // Registers a belt/splitter/tunnel building's tile with the belt subsystem
    // (on construction completion, or when un-queuing a deconstruction). No-op for
    // non-belt-subsystem types. Splitter filters are (re)applied after placement.
    void                 reregisterBeltTile(const Building& building,
                                            const std::vector<ItemType>& splitterFilterA,
                                            const std::vector<ItemType>& splitterFilterB);

    Building*            findBuildingMutable(BuildingId id);
    // True if the consumer would accept `type` at the given input port right now:
    // it is a required input (or a building block for the HQ), the reservation-aware
    // buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE).
    bool                 canAcceptInput(const Building& consumer,
                                        std::size_t inputPortIndex,
                                        const ItemType& type) const;
    // Places an accepted item onto the consumer's input belt at progress 0.0,
    // reserving a per-material buffer slot (REQ-MAT-INPUT-INTAKE).
    void                 depositToInputBelt(Building& consumer,
                                            std::size_t inputPortIndex,
                                            const Item& item);
    // Attempts to hand an emerging output item straight into a directly adjacent
    // building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
    // Returns true if the item was accepted onto the consumer's input belt.
    bool                 tryDirectCoupleDeposit(BuildingId producerId,
                                                const Port& outputPort,
                                                const Item& item);

    // Candidate recipes an idle building would try this tick: an auto-recipe
    // building (Smelter, Reprocessing Plant) offers every recipe of its type with
    // inputs; other buildings offer only their selected recipe. Shared by
    // tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT).
    std::vector<const RecipeDef*> gatherCandidateRecipes(const Building& b) const;
    // True if every input of `recipe` is present in `b`'s input buffers in the
    // required per-cycle amount (REQ-MAT-CYCLE input check).
    bool                recipeInputsAvailable(const Building& b,
                                              const RecipeDef& recipe) const;
    // Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD).
    std::map<std::string, int> computeShipyardRequiredMaterials(const Building& b) const;
    // True if the building currently has all inputs/materials to start a cycle
    // (ignoring output-buffer space); drives the Starved/Blocked distinction of
    // the status light (REQ-UI-STATUS-LIGHT).
    bool                hasInputsToStart(const Building& b) const;

    void                initBuffers(Building& b, const RecipeDef& recipe) const;
    // Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
    // caps span the union of every recipe of the building's type; no player
    // recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
    void                initAutoBuffers(Building& b) const;
    void                initShipyardBuffers(Building& b) const;
    void                initSalvageBayBuffer(Building& b) const;
    std::vector<Port>   computeInputPorts(const Building& b) const;
    // Core input-edge scan shared by operational buildings and construction sites.
    std::vector<Port>   computeInputPorts(const std::vector<QPoint>& bodyCells,
                                          const std::vector<Port>& outputPorts) const;
    std::vector<Item>   rollReprocessingOutput(const RecipeDef& recipe);
    bool                bodyCellsWithinWorldBounds(
                            const std::vector<QPoint>& bodyCells,
                            QPoint anchor) const;

    const GameConfig&                              m_config;
    BeltSystem&                                    m_belts;
    std::function<BuildingId()>                      m_allocateBuildingId;
    std::function<void(int)>                       m_addBuildingBlocks;
    std::function<void(const std::string&, QVector2D,
                       const std::optional<ShipLayoutConfig>&)> m_spawnShip;
    std::function<bool(const std::string&)>        m_isItemUnlocked;
    std::mt19937&                                  m_rng;
    int                                            m_asteroidWidth_tiles;

    // The factory's world data — buildings, queued work, tile ownership. Held here
    // for now; the intent is for Simulation to own it and pass it into the tick
    // methods, leaving this system stateless over it (see FactoryState.h).
    FactoryState m_state;
};
