using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;

namespace SpaceGame.Api.Ships.AI;

public sealed partial class ShipAiService
{
    private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ShipSubTaskRuntime subTask)
    {
        if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId))
        {
            var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
            if (ship is not null)
            {
                return ship.Position;
            }

            var station = ResolveStation(world, subTask.TargetEntityId);
            if (station is not null)
            {
                return station.Position;
            }

            var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
            if (celestial is not null)
            {
                return celestial.Position;
            }

            var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
            if (wreck is not null)
            {
                return wreck.Position;
            }
        }

        return subTask.TargetPosition ?? Vector3.Zero;
    }

    private static CelestialRuntime? ResolveTravelTargetCelestial(SimulationWorld world, ShipSubTaskRuntime subTask, Vector3 targetPosition)
    {
        if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId))
        {
            var station = ResolveStation(world, subTask.TargetEntityId);
            if (station?.CelestialId is not null)
            {
                return world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId);
            }

            var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
            if (site?.CelestialId is not null)
            {
                return world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId);
            }

            var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
            if (celestial is not null)
            {
                return celestial;
            }

            if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) is { } wreck)
            {
                return world.Celestials
                  .Where(candidate => candidate.SystemId == wreck.SystemId)
                  .OrderBy(candidate => candidate.Position.DistanceTo(wreck.Position))
                  .FirstOrDefault();
            }
        }

        return world.Celestials
          .Where(candidate => subTask.TargetSystemId is null || candidate.SystemId == subTask.TargetSystemId)
          .OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
          .FirstOrDefault();
    }

    private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship)
    {
        if (ship.SpatialState.CurrentCelestialId is not null)
        {
            return world.Celestials.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentCelestialId);
        }

        return world.Celestials
          .Where(candidate => candidate.SystemId == ship.SystemId)
          .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
          .FirstOrDefault();
    }

    private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) =>
      world.Celestials.FirstOrDefault(candidate => candidate.SystemId == systemId && candidate.Kind == SpatialNodeKind.Star);

    private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) =>
      world.Systems.FirstOrDefault(candidate => candidate.Definition.Id == systemId)?.Position ?? Vector3.Zero;

    private static float GetLocalTravelSpeed(ShipRuntime ship) =>
      SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed) * GetSkillFactor(ship.Skills.Navigation);

    private static float GetWarpTravelSpeed(ShipRuntime ship) =>
      SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed) * GetSkillFactor(ship.Skills.Navigation);

    private static float GetSkillFactor(int skillLevel) =>
      Math.Clamp(1f + ((skillLevel - 3) * 0.08f), 0.75f, 1.4f);

    private static int GetEffectiveSkillLevel(
      SimulationWorld world,
      ShipRuntime ship,
      Func<ShipSkillProfileRuntime, int> captainSelector,
      Func<CommanderSkillProfileRuntime, int> managerSelector)
    {
        var captainLevel = captainSelector(ship.Skills);
        if (ship.CommanderId is null)
        {
            return captainLevel;
        }

        var shipCommander = world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId);
        var manager = shipCommander?.ParentCommanderId is null
          ? shipCommander
          : world.Commanders.FirstOrDefault(candidate => candidate.Id == shipCommander.ParentCommanderId) ?? shipCommander;
        return Math.Clamp((captainLevel + (manager is null ? 3 : managerSelector(manager.Skills)) + 1) / 2, 1, 5);
    }

    private static int ResolveBehaviorSystemRange(SimulationWorld world, ShipRuntime ship, string behaviorKind, int explicitRange)
    {
        if (explicitRange > 0)
        {
            return explicitRange;
        }

        var tradeSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Trade, skills => skills.Coordination);
        var miningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination);
        var combatSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Combat, skills => skills.Strategy);
        return behaviorKind switch
        {
            LocalAutoMine or LocalAutoTrade => 0,
            AdvancedAutoMine => Math.Clamp(1 + ((miningSkill - 1) / 2), 1, 3),
            AdvancedAutoTrade => Math.Clamp(1 + ((tradeSkill - 1) / 2), 1, 3),
            ExpertAutoMine => Math.Clamp(2 + ((miningSkill - 1) / 2), 2, Math.Max(world.Systems.Count - 1, 2)),
            FillShortages or FindBuildTasks or RevisitKnownStations or SupplyFleet => Math.Clamp(1 + ((tradeSkill + 1) / 2), 1, Math.Max(world.Systems.Count - 1, 1)),
            Patrol or Police or ProtectPosition or ProtectShip or ProtectStation => Math.Clamp(1 + ((combatSkill - 1) / 2), 1, Math.Max(world.Systems.Count - 1, 1)),
            _ => Math.Max(world.Systems.Count - 1, 0),
        };
    }

    private static int GetSystemDistanceTier(SimulationWorld world, string originSystemId, string targetSystemId)
    {
        if (string.Equals(originSystemId, targetSystemId, StringComparison.Ordinal))
        {
            return 0;
        }

        var originPosition = ResolveSystemGalaxyPosition(world, originSystemId);
        return world.Systems
          .OrderBy(system => system.Position.DistanceTo(originPosition))
          .ThenBy(system => system.Definition.Id, StringComparer.Ordinal)
          .Select(system => system.Definition.Id)
          .TakeWhile(systemId => !string.Equals(systemId, targetSystemId, StringComparison.Ordinal))
          .Count();
    }

    private static bool IsWithinSystemRange(SimulationWorld world, string originSystemId, string targetSystemId, int maxRange) =>
      maxRange < 0 || GetSystemDistanceTier(world, originSystemId, targetSystemId) <= maxRange;

    private static float GetShipDamagePerSecond(ShipRuntime ship) =>
      ship.Definition.Type switch
      {
          ShipType.Frigate => FrigateDps,
          ShipType.Destroyer => DestroyerDps,
          ShipType.Battleship => CruiserDps,
          ShipType.Carrier => CapitalDps,
          _ => 4f,
      };

    private static MiningOpportunity? SelectMiningOpportunity(
      SimulationWorld world,
      ShipRuntime ship,
      StationRuntime homeStation,
      CommanderAssignmentRuntime? assignment,
      string behaviorKind)
    {
        var policy = ResolvePolicy(world, ship.PolicySetId);
        var preferredItemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId;
        var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange);
        var effectiveMiningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination);
        string? deniedReason = null;
        var opportunity = world.Nodes
          .Where(node =>
          {
              if (node.OreRemaining <= 0.01f || !CanExtractNode(ship, node, world) || (preferredItemId is not null && !string.Equals(node.ItemId, preferredItemId, StringComparison.Ordinal)))
              {
                  return false;
              }

              if (!TryCheckSystemAllowed(world, policy, ship.FactionId, node.SystemId, "military", out var reason))
              {
                  deniedReason ??= reason;
                  return false;
              }

              return IsWithinSystemRange(world, homeStation.SystemId, node.SystemId, rangeBudget);
          })
          .Select(node =>
          {
              var buyer = SelectBestDeliveryStation(world, ship, node.ItemId, homeStation, behaviorKind);
              var demandScore = GetFactionDemandScore(world, ship.FactionId, node.ItemId);
              var distancePenalty = GetSystemDistanceTier(world, homeStation.SystemId, node.SystemId) * 18f;
              var routeRiskPenalty = GeopoliticalSimulationService.GetSystemRouteRisk(world, node.SystemId, ship.FactionId) * 30f;
              var score = (node.SystemId == homeStation.SystemId ? 55f : 0f)
            + (node.OreRemaining * 0.025f)
            + (demandScore * (string.Equals(behaviorKind, ExpertAutoMine, StringComparison.Ordinal) ? 22f : 12f))
            + (effectiveMiningSkill * 10f)
            - distancePenalty
            - routeRiskPenalty
            - node.Position.DistanceTo(ship.Position);
              return new MiningOpportunity(node, buyer, score, $"Mine {node.ItemId} for {buyer.Label}");
          })
          .OrderByDescending(candidate => candidate.Score)
          .ThenBy(candidate => candidate.Node.Id, StringComparer.Ordinal)
          .FirstOrDefault();
        if (opportunity is null && deniedReason is not null)
        {
            ship.LastAccessFailureReason = deniedReason;
        }

        return opportunity;
    }

    private static TradeRoutePlan? SelectTradeRoute(
      SimulationWorld world,
      ShipRuntime ship,
      StationRuntime? homeStation,
      string behaviorKind,
      bool knownStationsOnly)
    {
        var policy = ResolvePolicy(world, ship.PolicySetId);
        var stationsById = world.Stations
          .Where(station => station.FactionId == ship.FactionId)
          .ToDictionary(station => station.Id, StringComparer.Ordinal);
        var originSystemId = homeStation?.SystemId ?? ship.SystemId;
        var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange);
        var effectiveTradeSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Trade, skills => skills.Coordination);
        var requireKnownStations = knownStationsOnly || string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal);
        string? deniedReason = null;

        var route = world.MarketOrders
          .Where(order =>
            order.FactionId == ship.FactionId &&
            order.Kind == MarketOrderKinds.Buy &&
            order.RemainingAmount > 0.01f)
          .Select(order =>
          {
              StationRuntime? destination = null;
              ConstructionSiteRuntime? destinationSite = null;
              if (order.StationId is not null && stationsById.TryGetValue(order.StationId, out var destinationStation))
              {
                  destination = destinationStation;
              }
              else if (order.ConstructionSiteId is not null)
              {
                  destinationSite = world.ConstructionSites.FirstOrDefault(site => site.Id == order.ConstructionSiteId);
                  if (destinationSite is not null)
                  {
                      destination = ResolveSupportStation(world, ship, destinationSite);
                  }
              }

              if (destination is null)
              {
                  return null;
              }

              if (!TryCheckSystemAllowed(world, policy, ship.FactionId, destination.SystemId, "trade", out var destinationDeniedReason))
              {
                  deniedReason ??= destinationDeniedReason;
                  return null;
              }
              if (!IsWithinSystemRange(world, originSystemId, destination.SystemId, rangeBudget))
              {
                  return null;
              }
              if (requireKnownStations
              && ship.KnownStationIds.Count > 0
              && !ship.KnownStationIds.Contains(destination.Id)
              && (homeStation is null || !string.Equals(destination.Id, homeStation.Id, StringComparison.Ordinal)))
              {
                  return null;
              }
              if (string.Equals(behaviorKind, FindBuildTasks, StringComparison.Ordinal) && destinationSite is null)
              {
                  return null;
              }
              if (!string.Equals(behaviorKind, FindBuildTasks, StringComparison.Ordinal) && destinationSite is not null)
              {
                  return null;
              }

              var source = stationsById.Values
            .Where(station =>
            {
                if (station.Id == destination.Id || GetInventoryAmount(station.Inventory, order.ItemId) <= GetStationReserveFloor(world, station, order.ItemId) + 1f)
                {
                    return false;
                }

                if (!TryCheckSystemAllowed(world, policy, ship.FactionId, station.SystemId, "trade", out var sourceDeniedReason))
                {
                    deniedReason ??= sourceDeniedReason;
                    return false;
                }

                if (!IsWithinSystemRange(world, originSystemId, station.SystemId, rangeBudget))
                {
                    return false;
                }

                return !requireKnownStations
              || ship.KnownStationIds.Count == 0
              || ship.KnownStationIds.Contains(station.Id)
              || (homeStation is not null && string.Equals(station.Id, homeStation.Id, StringComparison.Ordinal));
            })
            .OrderByDescending(station => GetInventoryAmount(station.Inventory, order.ItemId) - GetStationReserveFloor(world, station, order.ItemId))
            .ThenByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0)
            .ThenBy(station => station.Id, StringComparer.Ordinal)
            .FirstOrDefault();
              if (source is null)
              {
                  return null;
              }

              var shortageBias = string.Equals(behaviorKind, FillShortages, StringComparison.Ordinal)
            ? GetFactionDemandScore(world, ship.FactionId, order.ItemId) * 35f
            : 0f;
              var buildBias = destinationSite is null ? 0f : 65f;
              var revisitBias = string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal) && ship.KnownStationIds.Contains(source.Id) && ship.KnownStationIds.Contains(destination.Id)
            ? 28f
            : 0f;
              var regionalNeedBias = GetRegionalCommodityPressure(world, ship.FactionId, destination.SystemId, order.ItemId) * 18f;
              var systemRangePenalty = (GetSystemDistanceTier(world, originSystemId, source.SystemId) + GetSystemDistanceTier(world, originSystemId, destination.SystemId)) * 16f;
              var riskPenalty =
            (GeopoliticalSimulationService.GetSystemRouteRisk(world, source.SystemId, ship.FactionId)
            + GeopoliticalSimulationService.GetSystemRouteRisk(world, destination.SystemId, ship.FactionId)) * 22f;
              var distanceScore = source.Position.DistanceTo(ship.Position) + source.Position.DistanceTo(destination.Position);
              var score = (order.Valuation * 50f)
            + shortageBias
            + buildBias
            + revisitBias
            + regionalNeedBias
            + (effectiveTradeSkill * 12f)
            - systemRangePenalty
            - riskPenalty
            - distanceScore;
              var summary = destinationSite is null
            ? $"{order.ItemId}: {source.Label} -> {destination.Label}"
            : $"{order.ItemId}: {source.Label} -> build support {destination.Label}";
              return new TradeRoutePlan(source, destination, order.ItemId, score, summary);
          })
          .Where(route => route is not null)
          .Cast<TradeRoutePlan>()
          .OrderByDescending(route => route.Score)
          .ThenBy(route => route.ItemId, StringComparer.Ordinal)
          .ThenBy(route => route.SourceStation.Id, StringComparer.Ordinal)
          .FirstOrDefault();
        if (route is null && deniedReason is not null)
        {
            ship.LastAccessFailureReason = deniedReason;
        }

        return route;
    }

    private static FleetSupplyPlan? SelectFleetSupplyPlan(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation)
    {
        var assignment = ResolveAssignment(world, ship);
        var targetCandidates = world.Ships
          .Where(candidate =>
            candidate.Id != ship.Id &&
            candidate.FactionId == ship.FactionId &&
            candidate.Definition.GetTotalCargoCapacity() > 0.01f &&
            (assignment?.TargetEntityId is null || string.Equals(candidate.Id, assignment.TargetEntityId, StringComparison.Ordinal)))
          .OrderByDescending(candidate => IsMilitaryShip(candidate.Definition) ? 1 : 0)
          .ThenBy(candidate => candidate.Id, StringComparer.Ordinal)
          .ToList();
        if (targetCandidates.Count == 0)
        {
            return null;
        }

        var sourceStations = world.Stations
          .Where(station => station.FactionId == ship.FactionId)
          .OrderByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0)
          .ThenBy(station => station.Id, StringComparer.Ordinal)
          .ToList();
        foreach (var target in targetCandidates)
        {
            var itemId = assignment?.ItemId
              ?? sourceStations
                .SelectMany(station => station.Inventory)
                .Where(entry => entry.Value > 2f)
                .OrderByDescending(entry => entry.Value)
                .ThenBy(entry => entry.Key, StringComparer.Ordinal)
                .Select(entry => entry.Key)
                .FirstOrDefault();
            if (itemId is null)
            {
                continue;
            }

            var source = sourceStations.FirstOrDefault(station => GetInventoryAmount(station.Inventory, itemId) > 2f);
            if (source is null)
            {
                continue;
            }

            var amount = MathF.Min(MathF.Max(10f, ship.Definition.GetTotalCargoCapacity() * 0.5f), GetInventoryAmount(source.Inventory, itemId));
            return new FleetSupplyPlan(source, target, itemId, amount, MathF.Max(16f, ship.DefaultBehavior.Radius), $"Supply {target.Definition.Name} with {itemId}");
        }

        return null;
    }

    private static StationRuntime? SelectKnownStationVisit(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation)
    {
        var candidateIds = ship.KnownStationIds.Count == 0 && homeStation is not null
          ? [homeStation.Id]
          : ship.KnownStationIds.OrderBy(id => id, StringComparer.Ordinal).ToArray();
        return candidateIds
          .Select(id => ResolveStation(world, id))
          .Where(station => station is not null && station.FactionId == ship.FactionId)
          .Cast<StationRuntime>()
          .OrderByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0)
          .ThenBy(station => station.SystemId == ship.SystemId ? 0 : 1)
          .ThenBy(station => station.Position.DistanceTo(ship.Position))
          .FirstOrDefault();
    }

    private static StationRuntime SelectBestDeliveryStation(SimulationWorld world, ShipRuntime ship, string itemId, StationRuntime homeStation, string behaviorKind)
    {
        if (!string.Equals(behaviorKind, ExpertAutoMine, StringComparison.Ordinal))
        {
            return homeStation;
        }

        return world.Stations
          .Where(station => station.FactionId == ship.FactionId)
          .OrderByDescending(station => GetFactionDemandScore(world, ship.FactionId, itemId) + GetRegionalCommodityPressure(world, ship.FactionId, station.SystemId, itemId) + (station.Id == homeStation.Id ? 5f : 0f))
          .ThenBy(station => station.SystemId == homeStation.SystemId ? 0 : 1)
          .ThenBy(station => station.Id, StringComparer.Ordinal)
          .FirstOrDefault()
          ?? homeStation;
    }

    private static ResourceNodeRuntime? SelectLocalMiningNode(SimulationWorld world, ShipRuntime ship, string systemId, string itemId)
    {
        var policy = ResolvePolicy(world, ship.PolicySetId);
        string? deniedReason = null;
        var node = world.Nodes
          .Where(candidate =>
          {
              if (!string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal)
                  || !string.Equals(candidate.ItemId, itemId, StringComparison.Ordinal)
                  || candidate.OreRemaining <= 0.01f
                  || !CanExtractNode(ship, candidate, world))
              {
                  return false;
              }

              if (!TryCheckSystemAllowed(world, policy, ship.FactionId, candidate.SystemId, "military", out var reason))
              {
                  deniedReason ??= reason;
                  return false;
              }

              return true;
          })
          .OrderByDescending(candidate => candidate.OreRemaining)
          .ThenBy(candidate => candidate.Position.DistanceTo(ship.Position))
          .ThenBy(candidate => candidate.Id, StringComparer.Ordinal)
          .FirstOrDefault();
        if (node is null && deniedReason is not null)
        {
            ship.LastAccessFailureReason = deniedReason;
        }

        return node;
    }

    private static StationRuntime? SelectLocalAutoMineBuyer(SimulationWorld world, ShipRuntime ship, string systemId, string itemId)
    {
        var policy = ResolvePolicy(world, ship.PolicySetId);
        var stationsById = world.Stations.ToDictionary(station => station.Id, StringComparer.Ordinal);
        string? deniedReason = null;
        var buyer = world.MarketOrders
          .Where(order =>
            order.Kind == MarketOrderKinds.Buy
            && string.Equals(order.ItemId, itemId, StringComparison.Ordinal)
            && order.RemainingAmount > 0.01f)
          .Select(order =>
          {
              StationRuntime? destination = null;
              if (order.StationId is not null && stationsById.TryGetValue(order.StationId, out var station))
              {
                  destination = station;
              }
              else if (order.ConstructionSiteId is not null)
              {
                  var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == order.ConstructionSiteId);
                  if (site is not null)
                  {
                      destination = ResolveSupportStation(world, ship, site);
                  }
              }

              if (destination is null || !string.Equals(destination.SystemId, systemId, StringComparison.Ordinal))
              {
                  return null;
              }

              if (!TryCheckSystemAllowed(world, policy, ship.FactionId, destination.SystemId, "trade", out var reason))
              {
                  deniedReason ??= reason;
                  return null;
              }

              var score = (order.Valuation * 20f)
                + MathF.Min(order.RemainingAmount, ship.Definition.GetTotalCargoCapacity())
                - destination.Position.DistanceTo(ship.Position);
              return new LocalMiningBuyerCandidate(destination, score);
          })
          .Where(candidate => candidate is not null)
          .Cast<LocalMiningBuyerCandidate>()
          .OrderByDescending(candidate => candidate.Score)
          .ThenBy(candidate => candidate.Station.Id, StringComparer.Ordinal)
          .Select(candidate => candidate.Station)
          .FirstOrDefault();
        if (buyer is null && deniedReason is not null)
        {
            ship.LastAccessFailureReason = deniedReason;
        }

        return buyer;
    }

    private static float GetFactionDemandScore(SimulationWorld world, string factionId, string itemId)
    {
        var signal = CommanderPlanningService.FindFactionEconomicAssessment(world, factionId)?
          .CommoditySignals
          .FirstOrDefault(candidate => candidate.ItemId == itemId);
        var regionalBottleneckScore = world.Geopolitics?.EconomyRegions.Bottlenecks
          .Where(bottleneck => string.Equals(bottleneck.ItemId, itemId, StringComparison.Ordinal))
          .Join(
            world.Geopolitics.EconomyRegions.Regions.Where(region => string.Equals(region.FactionId, factionId, StringComparison.Ordinal)),
            bottleneck => bottleneck.RegionId,
            region => region.Id,
            (bottleneck, _) => bottleneck.Severity)
          .DefaultIfEmpty()
          .Max() ?? 0f;
        if (signal is null)
        {
            return regionalBottleneckScore * 8f;
        }

        return MathF.Max(0f, signal.BuyBacklog + signal.ReservedForConstruction + MathF.Max(0f, -signal.ProjectedNetRatePerSecond * 50f) + (regionalBottleneckScore * 8f));
    }

    private static float GetRegionalCommodityPressure(SimulationWorld world, string factionId, string systemId, string itemId)
    {
        var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, systemId);
        if (region is null)
        {
            return 0f;
        }

        var bottleneck = world.Geopolitics?.EconomyRegions.Bottlenecks
          .FirstOrDefault(candidate => string.Equals(candidate.RegionId, region.Id, StringComparison.Ordinal)
            && string.Equals(candidate.ItemId, itemId, StringComparison.Ordinal));
        var assessment = world.Geopolitics?.EconomyRegions.EconomicAssessments
          .FirstOrDefault(candidate => string.Equals(candidate.RegionId, region.Id, StringComparison.Ordinal));
        return (bottleneck?.Severity ?? 0f) + ((assessment?.ConstructionPressure ?? 0f) * 2f);
    }

    private static ThreatTargetCandidate? SelectThreatTarget(
      SimulationWorld world,
      ShipRuntime ship,
      string targetSystemId,
      Vector3 anchorPosition,
      float radius,
      string? excludeEntityId = null)
    {
        var policy = ResolvePolicy(world, ship.PolicySetId);
        return world.Ships
          .Where(candidate =>
            candidate.Id != excludeEntityId &&
            candidate.Health > 0f &&
            candidate.FactionId != ship.FactionId &&
            string.Equals(candidate.SystemId, targetSystemId, StringComparison.Ordinal) &&
            candidate.Position.DistanceTo(anchorPosition) <= radius * 1.75f)
          .Select(candidate => new ThreatTargetCandidate(
            candidate.Id,
            candidate.SystemId,
            candidate.Position,
            100f
              + (IsMilitaryShip(candidate.Definition) ? 30f : 0f)
              - candidate.Position.DistanceTo(anchorPosition)
              - candidate.Position.DistanceTo(ship.Position)
              + (string.Equals(policy?.CombatEngagementPolicy, "aggressive", StringComparison.OrdinalIgnoreCase) ? 12f : 0f)))
          .Concat(world.Stations
            .Where(candidate =>
              candidate.Id != excludeEntityId &&
              candidate.FactionId != ship.FactionId &&
              string.Equals(candidate.SystemId, targetSystemId, StringComparison.Ordinal) &&
              candidate.Position.DistanceTo(anchorPosition) <= radius * 2f)
            .Select(candidate => new ThreatTargetCandidate(candidate.Id, candidate.SystemId, candidate.Position, 45f - candidate.Position.DistanceTo(anchorPosition) * 0.2f)))
          .OrderByDescending(candidate => candidate.Score)
          .ThenBy(candidate => candidate.EntityId, StringComparer.Ordinal)
          .FirstOrDefault();
    }

    private static PoliceContactCandidate? SelectPoliceContact(SimulationWorld world, ShipRuntime ship, string systemId, Vector3 anchorPosition, float radius)
    {
        var policy = ResolvePolicy(world, ship.PolicySetId);
        return world.Ships
          .Where(candidate =>
            candidate.Id != ship.Id &&
            candidate.Health > 0f &&
            candidate.FactionId != ship.FactionId &&
            string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal) &&
            candidate.Position.DistanceTo(anchorPosition) <= radius * 1.5f)
          .Select(candidate =>
          {
              var engage = IsMilitaryShip(candidate.Definition)
            || string.Equals(policy?.CombatEngagementPolicy, "aggressive", StringComparison.OrdinalIgnoreCase);
              var score = (engage ? 80f : 40f)
            - candidate.Position.DistanceTo(anchorPosition)
            - candidate.Position.DistanceTo(ship.Position)
            + (IsTransportShip(candidate.Definition) ? 8f : 0f);
              return new PoliceContactCandidate(candidate.Id, candidate.SystemId, candidate.Position, engage, score);
          })
          .OrderByDescending(candidate => candidate.Score)
          .ThenBy(candidate => candidate.EntityId, StringComparer.Ordinal)
          .FirstOrDefault();
    }

    private static SalvageOpportunity? SelectSalvageOpportunity(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation)
    {
        if (homeStation is null)
        {
            return null;
        }

        var rangeBudget = ResolveBehaviorSystemRange(world, ship, AutoSalvage, ship.DefaultBehavior.MaxSystemRange > 0 ? ship.DefaultBehavior.MaxSystemRange : 1);
        return world.Wrecks
          .Where(wreck =>
            wreck.RemainingAmount > 0.01f &&
            IsWithinSystemRange(world, homeStation.SystemId, wreck.SystemId, rangeBudget))
          .Select(wreck => new SalvageOpportunity(
            wreck,
            (wreck.RemainingAmount * 3f) - wreck.Position.DistanceTo(ship.Position) - (GetSystemDistanceTier(world, homeStation.SystemId, wreck.SystemId) * 25f),
            $"Salvage {wreck.ItemId} from {wreck.SourceEntityId}"))
          .OrderByDescending(candidate => candidate.Score)
          .ThenBy(candidate => candidate.Wreck.Id, StringComparer.Ordinal)
          .FirstOrDefault();
    }

    private static (string SystemId, Vector3 Position)? ResolveObjectTarget(SimulationWorld world, string? entityId)
    {
        if (entityId is null)
        {
            return null;
        }

        if (world.Ships.FirstOrDefault(candidate => candidate.Id == entityId) is { } ship)
        {
            return (ship.SystemId, ship.Position);
        }

        if (ResolveStation(world, entityId) is { } station)
        {
            return (station.SystemId, station.Position);
        }

        if (world.Celestials.FirstOrDefault(candidate => candidate.Id == entityId) is { } celestial)
        {
            return (celestial.SystemId, celestial.Position);
        }

        if (world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == entityId) is { } site)
        {
            var position = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? Vector3.Zero;
            return (site.SystemId, position);
        }

        if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == entityId) is { } wreck)
        {
            return (wreck.SystemId, wreck.Position);
        }

        return null;
    }

    private static Vector3 GetFormationPosition(Vector3 anchorPosition, string seed, float radius)
    {
        var hash = Math.Abs(seed.Aggregate(17, (acc, c) => (acc * 31) + c));
        var angle = (hash % 360) * (MathF.PI / 180f);
        return new Vector3(
          anchorPosition.X + (MathF.Cos(angle) * radius),
          anchorPosition.Y,
          anchorPosition.Z + (MathF.Sin(angle) * radius));
    }

    private static TradeRoutePlan? ResolveTradeRoute(SimulationWorld world, string itemId, string sourceStationId, string destinationStationId)
    {
        var source = ResolveStation(world, sourceStationId);
        var destination = ResolveStation(world, destinationStationId);
        return source is null || destination is null ? null : new TradeRoutePlan(source, destination, itemId, 0f, $"{itemId}: {source.Label} -> {destination.Label}");
    }

    private static StationRuntime? ResolveStation(SimulationWorld world, string? stationId) =>
      stationId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == stationId);

    private static ResourceNodeRuntime? ResolveNode(SimulationWorld world, string? nodeId) =>
      nodeId is null ? null : world.Nodes.FirstOrDefault(candidate => candidate.Id == nodeId);

    private static PolicySetRuntime? ResolvePolicy(SimulationWorld world, string? policySetId) =>
      policySetId is null ? null : world.Policies.FirstOrDefault(policy => policy.Id == policySetId);

    private static bool IsSystemAllowed(
      SimulationWorld world,
      PolicySetRuntime? policy,
      string factionId,
      string systemId,
      string accessKind) =>
      TryCheckSystemAllowed(world, policy, factionId, systemId, accessKind, out _);

    private static bool TryCheckSystemAllowed(
      SimulationWorld world,
      PolicySetRuntime? policy,
      string factionId,
      string systemId,
      string accessKind,
      out string? denialReason)
    {
        denialReason = null;
        if (policy?.BlacklistedSystemIds.Contains(systemId) == true)
        {
            denialReason = $"blacklisted:{systemId}";
            return false;
        }

        var controlState = GeopoliticalSimulationService.GetSystemControlState(world, systemId);
        var authorityFactionId = controlState?.ControllerFactionId ?? controlState?.PrimaryClaimantFactionId;
        if (authorityFactionId is null || string.Equals(authorityFactionId, factionId, StringComparison.Ordinal))
        {
            return true;
        }

        var hasAccess = string.Equals(accessKind, "trade", StringComparison.Ordinal)
          ? GeopoliticalSimulationService.HasTradeAccess(world, factionId, authorityFactionId)
          : GeopoliticalSimulationService.HasMilitaryAccess(world, factionId, authorityFactionId);
        if (!hasAccess)
        {
            denialReason = $"{accessKind}-access-denied:{authorityFactionId}";
            return false;
        }

        if (policy?.AvoidHostileSystems != true)
        {
            return true;
        }

        if (GeopoliticalSimulationService.HasHostileRelation(world, factionId, authorityFactionId))
        {
            denialReason = $"hostile-authority:{authorityFactionId}";
            return false;
        }

        var hostileInfluencer = controlState?.InfluencingFactionIds.FirstOrDefault(candidate =>
          !string.Equals(candidate, factionId, StringComparison.Ordinal)
          && GeopoliticalSimulationService.HasHostileRelation(world, factionId, candidate));
        if (hostileInfluencer is not null)
        {
            denialReason = $"hostile-influence:{hostileInfluencer}";
            return false;
        }

        return true;
    }

    private static CommanderAssignmentRuntime? ResolveAssignment(SimulationWorld world, ShipRuntime ship) =>
      ship.CommanderId is null
        ? null
        : world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId)?.Assignment;

    private static ShipPlanStepRuntime? GetCurrentStep(ShipPlanRuntime? plan) =>
      plan is null || plan.CurrentStepIndex >= plan.Steps.Count ? null : plan.Steps[plan.CurrentStepIndex];

    private static StationRuntime? ResolveSupportStation(SimulationWorld world, ShipRuntime ship, ConstructionSiteRuntime site)
    {
        return ResolveStation(world, ResolveAssignment(world, ship)?.HomeStationId ?? ship.DefaultBehavior.HomeStationId)
          ?? world.Stations
            .Where(station => station.FactionId == ship.FactionId)
            .OrderByDescending(station => station.SystemId == site.SystemId ? 1 : 0)
            .ThenBy(station => station.Id, StringComparer.Ordinal)
            .FirstOrDefault();
    }

    private static Vector3 ResolveSupportPosition(ShipRuntime ship, StationRuntime station, ConstructionSiteRuntime? site, SimulationWorld world)
    {
        if (ship.DockedStationId is not null)
        {
            return GetShipDockedPosition(ship, station);
        }

        if (site?.StationId is null && site is not null)
        {
            var anchorPosition = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? station.Position;
            return GetResourceHoldPosition(anchorPosition, ship.Id, 78f);
        }

        return GetConstructionHoldPosition(station, ship.Id);
    }

    private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) =>
      ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f);

    private static void TrackHistory(ShipRuntime ship)
    {
        var plan = ship.ActivePlan;
        var step = GetCurrentStep(plan);
        var subTask = step is null || step.CurrentSubTaskIndex >= step.SubTasks.Count ? null : step.SubTasks[step.CurrentSubTaskIndex];
        var signature = $"{ship.State.ToContractValue()}|{plan?.Kind ?? "none"}|{step?.Kind ?? "none"}|{subTask?.Kind ?? "none"}|{GetShipCargoAmount(ship):0.0}";
        if (ship.LastSignature == signature)
        {
            return;
        }

        ship.LastSignature = signature;
        ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} plan={plan?.Kind ?? "none"} step={step?.Kind ?? "none"} subTask={subTask?.Kind ?? "none"} cargo={GetShipCargoAmount(ship):0.#}");
        if (ship.History.Count > 24)
        {
            ship.History.RemoveAt(0);
        }
    }

    private static void EmitStateEvents(ShipRuntime ship, ShipState previousState, string? previousPlanId, string? previousStepId, ICollection<SimulationEventRecord> events)
    {
        var currentPlanId = ship.ActivePlan?.Id;
        var currentStepId = GetCurrentStep(ship.ActivePlan)?.Id;
        var occurredAtUtc = DateTimeOffset.UtcNow;
        if (previousState != ship.State)
        {
            events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Name} state {previousState.ToContractValue()} -> {ship.State.ToContractValue()}.", occurredAtUtc));
        }

        if (!string.Equals(previousPlanId, currentPlanId, StringComparison.Ordinal))
        {
            events.Add(new SimulationEventRecord("ship", ship.Id, "plan-changed", $"{ship.Definition.Name} switched active plan.", occurredAtUtc));
        }

        if (!string.Equals(previousStepId, currentStepId, StringComparison.Ordinal))
        {
            events.Add(new SimulationEventRecord("ship", ship.Id, "step-changed", $"{ship.Definition.Name} advanced plan step.", occurredAtUtc));
        }
    }

    private static void CompleteStationFoundation(SimulationWorld world, StationRuntime supportStation, ConstructionSiteRuntime site)
    {
        var anchor = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId);
        if (anchor is null || site.BlueprintId is null)
        {
            site.State = ConstructionSiteStateKinds.Destroyed;
            return;
        }

        var station = new StationRuntime
        {
            Id = $"station-{world.Stations.Count + 1}",
            SystemId = site.SystemId,
            Label = BuildFoundedStationLabel(site.TargetDefinitionId),
            Category = "station",
            Objective = DetermineFoundationObjective(site.TargetDefinitionId),
            Color = world.Factions.FirstOrDefault(candidate => candidate.Id == site.FactionId)?.Color ?? supportStation.Color,
            Position = anchor.Position,
            FactionId = site.FactionId,
            CelestialId = site.CelestialId,
            Health = 600f,
            MaxHealth = 600f,
        };

        foreach (var moduleId in GetFoundationModules(world, site.BlueprintId))
        {
            AddStationModule(world, station, moduleId);
        }

        world.Stations.Add(station);
        StationLifecycleService.EnsureStationCommander(world, station);
        anchor.OccupyingStructureId = station.Id;
        site.StationId = station.Id;
        PrepareNextConstructionSiteStep(world, station, site);
    }

    private static IReadOnlyList<string> GetFoundationModules(SimulationWorld world, string primaryModuleId)
    {
        var modules = new List<string> { "module_arg_dock_m_01_lowtech" };
        foreach (var itemId in world.ProductionGraph.OutputsByModuleId.GetValueOrDefault(primaryModuleId, []))
        {
            if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
            {
                var storageModule = GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind);
                if (storageModule is not null && !modules.Contains(storageModule, StringComparer.Ordinal))
                {
                    modules.Add(storageModule);
                }
            }
        }

        if (!modules.Contains("module_arg_stor_container_m_01", StringComparer.Ordinal))
        {
            modules.Add("module_arg_stor_container_m_01");
        }

        if (!string.Equals(primaryModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
        {
            modules.Add("module_gen_prod_energycells_01");
        }

        modules.Add(primaryModuleId);
        return modules.Distinct(StringComparer.Ordinal).ToList();
    }

    private static string DetermineFoundationObjective(string commodityId) =>
      commodityId switch
      {
          "energycells" => "power",
          "water" => "water",
          "refinedmetals" => "refinery",
          "hullparts" => "hullparts",
          "claytronics" => "claytronics",
          "shipyard" => "shipyard",
          _ => "general",
      };

    private static string BuildFoundedStationLabel(string commodityId) =>
      $"{char.ToUpperInvariant(commodityId[0])}{commodityId[1..]} Foundry";
}
