using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;

namespace SpaceGame.Api.Stations.Simulation;

internal sealed class InfrastructureSimulationService
{
    private const float CommodityTargetLevelSeconds = 240f;
    private const float EnergyTargetLevelSeconds = 240f;

    internal void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
    {
        foreach (var claim in world.Claims)
        {
            if (claim.State == ClaimStateKinds.Destroyed || claim.Health <= 0f)
            {
                if (claim.State != ClaimStateKinds.Destroyed)
                {
                    claim.State = ClaimStateKinds.Destroyed;
                    events.Add(new SimulationEventRecord("claim", claim.Id, "claim-destroyed", $"Claim {claim.Id} was destroyed.", world.GeneratedAtUtc));
                }

                foreach (var site in world.ConstructionSites.Where(candidate => candidate.ClaimId == claim.Id))
                {
                    site.State = ConstructionSiteStateKinds.Destroyed;
                }

                continue;
            }

            if (claim.State == ClaimStateKinds.Activating && world.GeneratedAtUtc >= claim.ActivatesAtUtc)
            {
                claim.State = ClaimStateKinds.Active;
                events.Add(new SimulationEventRecord("claim", claim.Id, "claim-activated", $"Claim {claim.Id} is now active.", world.GeneratedAtUtc));
            }
        }
    }

    internal void UpdateConstructionSites(SimulationWorld world, ICollection<SimulationEventRecord> events)
    {
        foreach (var site in world.ConstructionSites)
        {
            if (site.State == ConstructionSiteStateKinds.Destroyed)
            {
                continue;
            }

            var claim = site.ClaimId is null
                ? null
                : world.Claims.FirstOrDefault(candidate => candidate.Id == site.ClaimId);
            if (claim?.State == ClaimStateKinds.Destroyed)
            {
                site.State = ConstructionSiteStateKinds.Destroyed;
                continue;
            }

            if (claim?.State == ClaimStateKinds.Active && site.State == ConstructionSiteStateKinds.Planned)
            {
                site.State = ConstructionSiteStateKinds.Active;
                events.Add(new SimulationEventRecord("construction-site", site.Id, "site-active", $"Construction site {site.Id} is active.", world.GeneratedAtUtc));
            }

            foreach (var orderId in site.MarketOrderIds)
            {
                var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
                if (order is null || !site.RequiredItems.TryGetValue(order.ItemId, out var required))
                {
                    continue;
                }

                var remaining = MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, order.ItemId));
                order.RemainingAmount = remaining;
                order.State = remaining <= 0.01f
                    ? MarketOrderStateKinds.Filled
                    : remaining < order.Amount
                        ? MarketOrderStateKinds.PartiallyFilled
                        : MarketOrderStateKinds.Open;
            }
        }
    }

    internal static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId)
    {
        if (station.ActiveConstruction is not null)
        {
            return string.Equals(station.ActiveConstruction.ModuleId, recipe.ModuleId, StringComparison.Ordinal)
                && string.Equals(station.ActiveConstruction.AssignedConstructorShipId, shipId, StringComparison.Ordinal);
        }

        if (!CanStartModuleConstruction(station, recipe))
        {
            return false;
        }

        foreach (var input in recipe.Inputs)
        {
            RemoveInventory(station.Inventory, input.ItemId, input.Amount);
        }

        station.ActiveConstruction = new ModuleConstructionRuntime
        {
            ModuleId = recipe.ModuleId,
            RequiredSeconds = recipe.Duration,
            AssignedConstructorShipId = shipId,
        };

        return true;
    }

    internal static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world)
    {
        var economy = FactionEconomyAnalyzer.Build(world, station.FactionId);
        return GetModuleExpansionCandidates(world, station, economy)
          .Where(candidate => world.ModuleRecipes.ContainsKey(candidate.ModuleId))
          .OrderByDescending(candidate => candidate.Score)
          .Select(candidate => candidate.ModuleId)
          .FirstOrDefault();
    }

    private static IReadOnlyList<ModuleExpansionCandidate> GetModuleExpansionCandidates(
        SimulationWorld world,
        StationRuntime station,
        FactionEconomySnapshot economy)
    {
        var role = StationSimulationService.DetermineStationRole(station);
        var candidates = new Dictionary<string, float>(StringComparer.Ordinal);
        var constructionDemandByItem = GetOutstandingConstructionDemand(world, station.FactionId);
        var objectiveCommodity = GetObjectiveCommodityId(role);
        var objectiveModuleId = GetObjectiveModuleId(world, role, objectiveCommodity);

        if (objectiveModuleId is not null && world.ModuleRecipes.TryGetValue(objectiveModuleId, out var objectiveRecipe))
        {
            AddOrRaiseCandidate(candidates, objectiveModuleId, ScoreObjectiveModule(world, station, economy, constructionDemandByItem, objectiveCommodity, objectiveModuleId));

            foreach (var storageModuleId in GetRequiredStorageModules(world, objectiveRecipe))
            {
                if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal))
                {
                    AddOrRaiseCandidate(candidates, storageModuleId, ScoreStorageModule(world, station, storageModuleId, objectiveModuleId, objectiveCommodity, requiredByObjective: true));
                }
            }

            if (objectiveCommodity is not null
                && world.ProductionGraph.GetImmediateInputs(objectiveCommodity).Contains("energycells", StringComparer.Ordinal))
            {
                AddOrRaiseCandidate(candidates, "module_gen_prod_energycells_01", ScoreEnergySupportModule(world, station, economy, constructionDemandByItem));
            }
        }

        AddOrRaiseCandidate(candidates, "module_arg_dock_m_01_lowtech", ScoreDockModule(station));
        AddOrRaiseCandidate(candidates, "module_arg_hab_m_01", ScoreHabitationModule(station, world, economy));

        foreach (var storageModuleId in GetStoragePressureCandidates(world, station))
        {
            AddOrRaiseCandidate(candidates, storageModuleId, ScoreStorageModule(world, station, storageModuleId, objectiveModuleId, objectiveCommodity, requiredByObjective: false));
        }

        return candidates
          .Where(entry => entry.Value > 0.01f)
          .Select(entry => new ModuleExpansionCandidate(entry.Key, entry.Value))
          .ToList();
    }

    private static IEnumerable<string> GetStoragePressureCandidates(SimulationWorld world, StationRuntime station)
    {
        foreach (var (storageKind, moduleId) in new[]
        {
      (StorageKind.Solid, "module_arg_stor_solid_m_01"),
      (StorageKind.Liquid, "module_arg_stor_liquid_m_01"),
      (StorageKind.Container, "module_arg_stor_container_m_01"),
    })
        {
            var capacity = GetStationStorageCapacity(world, station, storageKind);
            if (capacity <= 0.01f)
            {
                continue;
            }

            var used = station.Inventory
              .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageKind)
              .Sum(entry => entry.Value);
            if (used / capacity >= 0.65f)
            {
                yield return moduleId;
            }
        }
    }

    private static IEnumerable<string> GetRequiredStorageModules(SimulationWorld world, ModuleRecipeDefinition recipe)
    {
        var itemIds = recipe.Inputs.Select(input => input.ItemId);
        foreach (var itemId in itemIds)
        {
            if (!world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
            {
                continue;
            }

            if (GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
            {
                yield return storageModuleId;
            }
        }

        if (world.ModuleDefinitions.TryGetValue(recipe.ModuleId, out var moduleDefinition))
        {
            foreach (var productItemId in moduleDefinition.ProductItemIds)
            {
                if (!world.ItemDefinitions.TryGetValue(productItemId, out var itemDefinition))
                {
                    continue;
                }

                if (GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
                {
                    yield return storageModuleId;
                }
            }
        }
    }

    private static string? GetObjectiveCommodityId(string role) =>
        role switch
        {
            "power" => "energycells",
            "refinery" => "refinedmetals",
            "water" => "water",
            "graphene" => "graphene",
            "siliconwafers" => "siliconwafers",
            "hullparts" => "hullparts",
            "claytronics" => "claytronics",
            "quantumtubes" => "quantumtubes",
            "antimattercells" => "antimattercells",
            "superfluidcoolant" => "superfluidcoolant",
            _ => null,
        };

    private static string? GetObjectiveModuleId(SimulationWorld world, string role, string? objectiveCommodityId) =>
        role switch
        {
            "shipyard" => "module_gen_build_l_01",
            _ => objectiveCommodityId is null ? null : world.ProductionGraph.GetPrimaryProducerModule(objectiveCommodityId),
        };

    private static float ScoreObjectiveModule(
        SimulationWorld world,
        StationRuntime station,
        FactionEconomySnapshot economy,
        IReadOnlyDictionary<string, float> constructionDemandByItem,
        string? objectiveCommodityId,
        string objectiveModuleId)
    {
        if (string.IsNullOrWhiteSpace(objectiveCommodityId))
        {
            var hasShipyard = CountModules(station.InstalledModules, objectiveModuleId);
            return hasShipyard == 0 ? 240f : 0f;
        }

        var commodity = economy.GetCommodity(objectiveCommodityId);
        var currentCount = CountModules(station.InstalledModules, objectiveModuleId);
        var marginalOutputRate = EstimateMarginalOutputRate(world, station, objectiveModuleId, objectiveCommodityId);
        var constructionImpact = EstimateConstructionBottleneckImpact(world, objectiveModuleId, constructionDemandByItem);
        var score = 90f
          + CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(objectiveCommodityId))
          + (marginalOutputRate * 900f)
          + constructionImpact;

        if (currentCount == 0)
        {
            score += 80f;
        }

        if (commodity.LevelSeconds < GetTargetLevelSeconds(objectiveCommodityId))
        {
            score += MathF.Max(0f, GetTargetLevelSeconds(objectiveCommodityId) - commodity.LevelSeconds) * 0.3f;
        }

        score *= EstimateObjectiveExpansionFeasibility(world, station, economy, objectiveModuleId, objectiveCommodityId);
        score *= EstimateProducerReadiness(world, station, economy, objectiveModuleId, objectiveCommodityId);
        score += EstimateImmediateProducerActivationScore(world, station, economy, objectiveModuleId, objectiveCommodityId);
        return score - (currentCount * 35f);
    }

    private static float ScoreEnergySupportModule(
        SimulationWorld world,
        StationRuntime station,
        FactionEconomySnapshot economy,
        IReadOnlyDictionary<string, float> constructionDemandByItem)
    {
        var energy = economy.GetCommodity("energycells");
        var currentCount = CountModules(station.InstalledModules, "module_gen_prod_energycells_01");
        var constructionImpact = EstimateConstructionBottleneckImpact(world, "module_gen_prod_energycells_01", constructionDemandByItem);
        var readinessUnlock = EstimateSupportUnlockScore(world, station, economy, "module_gen_prod_energycells_01");
        var score = 40f
          + CommodityOperationalSignal.ComputeNeedScore(energy, EnergyTargetLevelSeconds) * 0.5f
          + constructionImpact
          + readinessUnlock;

        if (currentCount == 0)
        {
            score += 70f;
        }

        if (energy.LevelSeconds < EnergyTargetLevelSeconds)
        {
            score += MathF.Max(0f, EnergyTargetLevelSeconds - energy.LevelSeconds) * 0.2f;
        }

        return score - (currentCount * 40f);
    }

    private static float ScoreStorageModule(
        SimulationWorld world,
        StationRuntime station,
        string storageModuleId,
        string? objectiveModuleId,
        string? objectiveCommodityId,
        bool requiredByObjective)
    {
        var storageKind = storageModuleId switch
        {
            "module_arg_stor_solid_m_01" => StorageKind.Solid,
            "module_arg_stor_liquid_m_01" => StorageKind.Liquid,
            _ => StorageKind.Container,
        };

        var capacity = GetStationStorageCapacity(world, station, storageKind);
        var used = station.Inventory
          .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageKind)
          .Sum(entry => entry.Value);
        var utilization = capacity <= 0.01f ? 0f : used / capacity;

        var score = requiredByObjective ? 140f : 0f;
        score += MathF.Max(0f, utilization - 0.6f) * 240f;

        if (!string.IsNullOrWhiteSpace(objectiveModuleId) && !string.IsNullOrWhiteSpace(objectiveCommodityId))
        {
            var objectiveUsesStorage = ModuleNeedsStorageClass(world, objectiveModuleId, storageKind)
              || CommodityUsesStorageClass(world, objectiveCommodityId, storageKind);
            if (objectiveUsesStorage)
            {
                score += 35f;
                score += EstimateSupportUnlockScore(world, station, economy: null, supportModuleId: storageModuleId);
            }
        }

        return score;
    }

    private static float ScoreDockModule(StationRuntime station)
    {
        var dockingPads = GetDockingPadCount(station);
        var dockedShips = station.DockedShipIds.Count;
        if (dockingPads <= 0)
        {
            return 150f;
        }

        return dockedShips >= dockingPads ? 80f : dockingPads < 4 ? 25f : 0f;
    }

    private static float ScoreHabitationModule(StationRuntime station)
    {
        if (station.WorkforceRequired <= 0.01f)
        {
            return 0f;
        }

        return station.WorkforceEffectiveRatio < 0.75f
          ? 30f
          : station.WorkforceEffectiveRatio < 0.95f
              ? 10f
              : 0f;
    }

    private static float ScoreHabitationModule(StationRuntime station, SimulationWorld world, FactionEconomySnapshot economy)
    {
        return ScoreHabitationModule(station) + EstimateSupportUnlockScore(world, station, economy, "module_arg_hab_m_01");
    }

    private static void AddOrRaiseCandidate(IDictionary<string, float> candidates, string moduleId, float score)
    {
        if (score <= 0.01f)
        {
            return;
        }

        if (!candidates.TryGetValue(moduleId, out var existing) || score > existing)
        {
            candidates[moduleId] = score;
        }
    }

    private static float EstimateMarginalOutputRate(
        SimulationWorld world,
        StationRuntime station,
        string moduleId,
        string commodityId)
    {
        var recipe = world.Recipes.Values
          .Where(recipe =>
              string.Equals(StationSimulationService.GetStationProductionLaneKey(world, recipe), moduleId, StringComparison.Ordinal)
              && StationSimulationService.RecipeAppliesToStation(station, recipe))
          .Where(recipe => recipe.Outputs.Any(output => string.Equals(output.ItemId, commodityId, StringComparison.Ordinal)))
          .OrderByDescending(recipe => recipe.Priority)
          .FirstOrDefault();

        if (recipe is null)
        {
            return 0f;
        }

        var amount = recipe.Outputs
          .Where(output => string.Equals(output.ItemId, commodityId, StringComparison.Ordinal))
          .Sum(output => output.Amount);
        return amount * station.WorkforceEffectiveRatio / MathF.Max(recipe.Duration, 0.01f);
    }

    private static float EstimateObjectiveExpansionFeasibility(
        SimulationWorld world,
        StationRuntime station,
        FactionEconomySnapshot economy,
        string moduleId,
        string commodityId)
    {
        var recipes = world.Recipes.Values
          .Where(recipe =>
              string.Equals(StationSimulationService.GetStationProductionLaneKey(world, recipe), moduleId, StringComparison.Ordinal)
              && StationSimulationService.RecipeAppliesToStation(station, recipe)
              && recipe.Outputs.Any(output => string.Equals(output.ItemId, commodityId, StringComparison.Ordinal)))
          .ToList();
        if (recipes.Count == 0)
        {
            return 1f;
        }

        var feasibility = 1f;
        foreach (var recipe in recipes)
        {
            foreach (var input in recipe.Inputs)
            {
                var inputCommodity = economy.GetCommodity(input.ItemId);
                feasibility *= CommodityOperationalSignal.ComputeFeasibilityFactor(
                  inputCommodity,
                  GetTargetLevelSeconds(input.ItemId));
            }
        }

        return Math.Clamp(feasibility, 0.35f, 1.15f);
    }

    private static float EstimateProducerReadiness(
        SimulationWorld world,
        StationRuntime station,
        FactionEconomySnapshot economy,
        string moduleId,
        string commodityId)
    {
        var analysis = AnalyzeProducerLane(world, station, economy, moduleId, commodityId);
        return analysis.Readiness;
    }

    private static ProducerLaneAnalysis AnalyzeProducerLane(
        SimulationWorld world,
        StationRuntime station,
        FactionEconomySnapshot economy,
        string moduleId,
        string commodityId)
    {
        var recipe = world.Recipes.Values
          .Where(recipe =>
              string.Equals(StationSimulationService.GetStationProductionLaneKey(world, recipe), moduleId, StringComparison.Ordinal)
              && StationSimulationService.RecipeAppliesToStation(station, recipe)
              && recipe.Outputs.Any(output => string.Equals(output.ItemId, commodityId, StringComparison.Ordinal)))
          .OrderByDescending(recipe => recipe.Priority)
          .FirstOrDefault();
        if (recipe is null)
        {
            return new ProducerLaneAnalysis(1f, 1f, false, false, false, false);
        }

        var workforceFactor = station.WorkforceEffectiveRatio < 0.45f
          ? 0.75f
          : station.WorkforceEffectiveRatio < 0.75f
              ? 0.88f
              : 1f;
        var inputFactor = 1f;
        var missingLocalInputs = false;
        var missingFactionInputs = false;

        foreach (var input in recipe.Inputs)
        {
            var localAmount = GetInventoryAmount(station.Inventory, input.ItemId);
            var commodity = economy.GetCommodity(input.ItemId);
            if (localAmount + 0.001f >= input.Amount)
            {
                continue;
            }

            missingLocalInputs = true;
            var shortage = input.Amount - localAmount;
            var availableStockRatio = commodity.AvailableStock <= 0.01f ? 0f : MathF.Min(1f, commodity.AvailableStock / MathF.Max(input.Amount, 0.01f));
            if (commodity.AvailableStock >= shortage)
            {
                inputFactor *= 0.95f + (availableStockRatio * 0.05f);
            }
            else if (commodity.ProjectedProductionRatePerSecond > 0.01f
              && commodity.Level is not CommodityLevelKind.Critical)
            {
                inputFactor *= 0.82f + (availableStockRatio * 0.08f);
            }
            else
            {
                inputFactor *= 0.55f + (availableStockRatio * 0.15f);
                missingFactionInputs = true;
            }
        }

        var outputReady = true;
        foreach (var output in recipe.Outputs)
        {
            if (!CanStationAcceptStationOutputSoon(world, station, output.ItemId, output.Amount))
            {
                outputReady = false;
            }
        }

        var readiness = Math.Clamp(workforceFactor * inputFactor * (outputReady ? 1f : 0.72f), 0.4f, 1.1f);
        return new ProducerLaneAnalysis(
          readiness,
          workforceFactor,
          missingLocalInputs,
          missingFactionInputs,
          !outputReady,
          outputReady && inputFactor >= 0.9f);
    }

    private static float EstimateSupportUnlockScore(
        SimulationWorld world,
        StationRuntime station,
        FactionEconomySnapshot? economy,
        string supportModuleId)
    {
        var role = StationSimulationService.DetermineStationRole(station);
        var objectiveCommodityId = GetObjectiveCommodityId(role);
        var objectiveModuleId = GetObjectiveModuleId(world, role, objectiveCommodityId);
        if (string.IsNullOrWhiteSpace(objectiveCommodityId) || string.IsNullOrWhiteSpace(objectiveModuleId))
        {
            return 0f;
        }

        var analysis = economy is null
          ? new ProducerLaneAnalysis(0.75f, 1f, false, false, false, false)
          : AnalyzeProducerLane(world, station, economy, objectiveModuleId, objectiveCommodityId);

        var unlockScore = 0f;
        switch (supportModuleId)
        {
            case "module_arg_hab_m_01" when analysis.WorkforceFactor < 0.9f
              && !analysis.HasMissingFactionInputs
              && !analysis.HasMissingOutputStorage:
                unlockScore += (1f - analysis.WorkforceFactor) * 150f;
                break;
            case "module_gen_prod_energycells_01":
                if (ObjectiveNeedsEnergy(world, objectiveCommodityId)
                    && analysis.HasMissingLocalInputs
                    && (economy?.GetCommodity("energycells").AvailableStock ?? 0f) < 120f)
                {
                    unlockScore += 90f;
                }
                break;
            case "module_arg_stor_container_m_01":
            case "module_arg_stor_solid_m_01":
            case "module_arg_stor_liquid_m_01":
                var storageKind = supportModuleId switch
                {
                    "module_arg_stor_solid_m_01" => StorageKind.Solid,
                    "module_arg_stor_liquid_m_01" => StorageKind.Liquid,
                    _ => StorageKind.Container,
                };
                if (analysis.HasMissingOutputStorage
                    && (ModuleNeedsStorageClass(world, objectiveModuleId, storageKind)
                        || CommodityUsesStorageClass(world, objectiveCommodityId, storageKind)))
                {
                    unlockScore += 70f;
                }
                break;
        }

        return unlockScore * MathF.Max(0.4f, 1f - analysis.Readiness);
    }

    private static float EstimateImmediateProducerActivationScore(
        SimulationWorld world,
        StationRuntime station,
        FactionEconomySnapshot economy,
        string moduleId,
        string commodityId)
    {
        var analysis = AnalyzeProducerLane(world, station, economy, moduleId, commodityId);
        if (analysis.CanRunSoon)
        {
            return 110f;
        }

        if (!analysis.HasMissingFactionInputs && !analysis.HasMissingOutputStorage)
        {
            return 45f * MathF.Max(0.6f, analysis.WorkforceFactor);
        }

        return 0f;
    }

    private static float EstimateConstructionBottleneckImpact(
        SimulationWorld world,
        string moduleId,
        IReadOnlyDictionary<string, float> constructionDemandByItem)
    {
        if (!world.ModuleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
        {
            return 0f;
        }

        var score = 0f;
        foreach (var productItemId in moduleDefinition.ProductItemIds)
        {
            if (!constructionDemandByItem.TryGetValue(productItemId, out var outstandingDemand) || outstandingDemand <= 0.01f)
            {
                continue;
            }

            var outputRate = EstimateModuleOutputRate(world, moduleId, productItemId);
            if (outputRate <= 0.0001f)
            {
                continue;
            }

            score += MathF.Min(outstandingDemand, outputRate * 900f) * 0.8f;
        }

        return score;
    }

    private static float EstimateModuleOutputRate(SimulationWorld world, string moduleId, string itemId)
    {
        var recipe = world.Recipes.Values
          .Where(recipe => string.Equals(StationSimulationService.GetStationProductionLaneKey(world, recipe), moduleId, StringComparison.Ordinal))
          .Where(recipe => recipe.Outputs.Any(output => string.Equals(output.ItemId, itemId, StringComparison.Ordinal)))
          .OrderByDescending(recipe => recipe.Priority)
          .FirstOrDefault();
        if (recipe is null)
        {
            return 0f;
        }

        return recipe.Outputs
          .Where(output => string.Equals(output.ItemId, itemId, StringComparison.Ordinal))
          .Sum(output => output.Amount) / MathF.Max(recipe.Duration, 0.01f);
    }

    private static IReadOnlyDictionary<string, float> GetOutstandingConstructionDemand(SimulationWorld world, string factionId)
    {
        var demand = new Dictionary<string, float>(StringComparer.Ordinal);

        foreach (var site in world.ConstructionSites.Where(site =>
            string.Equals(site.FactionId, factionId, StringComparison.Ordinal)
            && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed))
        {
            foreach (var required in site.RequiredItems)
            {
                var remaining = MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key));
                if (remaining <= 0.01f)
                {
                    continue;
                }

                demand[required.Key] = demand.GetValueOrDefault(required.Key) + remaining;
            }
        }

        return demand;
    }

    private static bool ModuleNeedsStorageClass(SimulationWorld world, string moduleId, StorageKind storageKind)
    {
        if (!world.ModuleRecipes.TryGetValue(moduleId, out var recipe))
        {
            return false;
        }

        return recipe.Inputs.Any(input =>
            world.ItemDefinitions.TryGetValue(input.ItemId, out var itemDefinition)
            && itemDefinition.CargoKind == storageKind);
    }

    private static bool CommodityUsesStorageClass(SimulationWorld world, string commodityId, StorageKind storageKind) =>
        world.ItemDefinitions.TryGetValue(commodityId, out var itemDefinition)
        && itemDefinition.CargoKind == storageKind;

    private static bool CanStationAcceptStationOutputSoon(SimulationWorld world, StationRuntime station, string itemId, float amount)
    {
        if (!world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
        {
            return false;
        }

        if (itemDefinition.CargoKind is not { } storageKind)
        {
            return false;
        }

        var capacity = GetStationStorageCapacity(world, station, storageKind);
        if (capacity <= 0.01f)
        {
            return false;
        }

        var used = station.Inventory
          .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageKind)
          .Sum(entry => entry.Value);
        return used + amount <= capacity * 0.95f;
    }

    private static bool ObjectiveNeedsEnergy(SimulationWorld world, string objectiveCommodityId) =>
        world.ProductionGraph.GetImmediateInputs(objectiveCommodityId).Contains("energycells", StringComparer.Ordinal);

    private static float GetTargetLevelSeconds(string commodityId) =>
        string.Equals(commodityId, "energycells", StringComparison.Ordinal) ? EnergyTargetLevelSeconds :
        string.Equals(commodityId, "water", StringComparison.Ordinal) ? 300f :
        string.Equals(commodityId, "graphene", StringComparison.Ordinal) ? 240f :
        string.Equals(commodityId, "siliconwafers", StringComparison.Ordinal) ? 240f :
        string.Equals(commodityId, "quantumtubes", StringComparison.Ordinal) ? 240f :
        string.Equals(commodityId, "antimattercells", StringComparison.Ordinal) ? 240f :
        string.Equals(commodityId, "superfluidcoolant", StringComparison.Ordinal) ? 240f :
        CommodityTargetLevelSeconds;

    internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
    {
        var nextModuleId = GetNextStationModuleToBuild(station, world);
        foreach (var orderId in site.MarketOrderIds)
        {
            var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
            if (order is not null)
            {
                order.State = MarketOrderStateKinds.Cancelled;
                order.RemainingAmount = 0f;
                world.MarketOrders.Remove(order);
            }

            station.MarketOrderIds.Remove(orderId);
        }

        site.MarketOrderIds.Clear();
        site.Inventory.Clear();
        site.DeliveredItems.Clear();
        site.RequiredItems.Clear();
        site.AssignedConstructorShipIds.Clear();
        site.Progress = 0f;

        if (nextModuleId is null || !world.ModuleRecipes.TryGetValue(nextModuleId, out var recipe))
        {
            site.State = ConstructionSiteStateKinds.Completed;
            site.BlueprintId = null;
            return;
        }

        site.BlueprintId = nextModuleId;
        site.State = ConstructionSiteStateKinds.Active;
        foreach (var input in recipe.Inputs)
        {
            site.RequiredItems[input.ItemId] = input.Amount;
            site.DeliveredItems[input.ItemId] = 0f;
            var orderId = $"market-order-{station.Id}-{nextModuleId}-{input.ItemId}";
            site.MarketOrderIds.Add(orderId);
            station.MarketOrderIds.Add(orderId);
            world.MarketOrders.Add(new MarketOrderRuntime
            {
                Id = orderId,
                FactionId = station.FactionId,
                StationId = station.Id,
                ConstructionSiteId = site.Id,
                Kind = MarketOrderKinds.Buy,
                ItemId = input.ItemId,
                Amount = input.Amount,
                RemainingAmount = input.Amount,
                Valuation = 1f,
                State = MarketOrderStateKinds.Open,
            });
        }
    }

    private sealed record ModuleExpansionCandidate(string ModuleId, float Score);

    private sealed record ProducerLaneAnalysis(
        float Readiness,
        float WorkforceFactor,
        bool HasMissingLocalInputs,
        bool HasMissingFactionInputs,
        bool HasMissingOutputStorage,
        bool CanRunSoon);

    internal static int GetDockingPadCount(StationRuntime station) =>
        CountModules(station.InstalledModules, "module_arg_dock_m_01_lowtech") * 2;

    internal static int? ReserveDockingPad(StationRuntime station, string shipId)
    {
        if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing
            && !string.IsNullOrEmpty(existing.Value))
        {
            return existing.Key;
        }

        var padCount = GetDockingPadCount(station);
        for (var padIndex = 0; padIndex < padCount; padIndex += 1)
        {
            if (station.DockingPadAssignments.ContainsKey(padIndex))
            {
                continue;
            }

            station.DockingPadAssignments[padIndex] = shipId;
            return padIndex;
        }

        return null;
    }

    internal static void ReleaseDockingPad(StationRuntime station, string shipId)
    {
        var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal));
        if (!string.IsNullOrEmpty(assignment.Value))
        {
            station.DockingPadAssignments.Remove(assignment.Key);
        }
    }

    internal static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex)
    {
        var padCount = Math.Max(1, GetDockingPadCount(station));
        var angle = ((MathF.PI * 2f) / padCount) * padIndex;
        var radius = station.Radius + 18f;
        return new Vector3(
            station.Position.X + (MathF.Cos(angle) * radius),
            station.Position.Y,
            station.Position.Z + (MathF.Sin(angle) * radius));
    }

    internal static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId)
    {
        var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
        var angle = (hash % 360) * (MathF.PI / 180f);
        var radius = station.Radius + 24f;
        return new Vector3(
            station.Position.X + (MathF.Cos(angle) * radius),
            station.Position.Y,
            station.Position.Z + (MathF.Sin(angle) * radius));
    }

    internal static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance)
    {
        if (padIndex is null)
        {
            return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
        }

        var pad = GetDockingPadPosition(station, padIndex.Value);
        var dx = pad.X - station.Position.X;
        var dz = pad.Z - station.Position.Z;
        var length = MathF.Sqrt((dx * dx) + (dz * dz));
        if (length <= 0.001f)
        {
            return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
        }

        var scale = distance / length;
        return new Vector3(
            pad.X + (dx * scale),
            station.Position.Y,
            pad.Z + (dz * scale));
    }

    internal static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) =>
        ship.AssignedDockingPadIndex is int padIndex
            ? GetDockingPadPosition(station, padIndex)
            : station.Position;

    internal static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId)
    {
        var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
        var angle = (hash % 360) * (MathF.PI / 180f);
        var radius = station.Radius + 78f;
        return new Vector3(
            station.Position.X + (MathF.Cos(angle) * radius),
            station.Position.Y,
            station.Position.Z + (MathF.Sin(angle) * radius));
    }

    internal static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius)
    {
        var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
        var angle = (hash % 360) * (MathF.PI / 180f);
        return new Vector3(
            nodePosition.X + (MathF.Cos(angle) * radius),
            nodePosition.Y,
            nodePosition.Z + (MathF.Sin(angle) * radius));
    }
}
