using System.Globalization;

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

namespace SpaceGame.Api.Geopolitics.Simulation;

internal sealed class GeopoliticalSimulationService
{
    internal void Update(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
    {
        var state = EnsureState(world);
        state.Cycle += 1;
        state.UpdatedAtUtc = world.GeneratedAtUtc;

        RebuildRoutes(world, state);
        RebuildTerritory(world, state);
        RebuildDiplomacy(world, state, events);
        RebuildEconomyRegions(world, state);
    }

    internal static GeopoliticalStateRuntime EnsureState(SimulationWorld world)
    {
        world.Geopolitics ??= new GeopoliticalStateRuntime();
        return world.Geopolitics;
    }

    internal static DiplomaticRelationRuntime? FindRelation(SimulationWorld world, string factionAId, string factionBId)
    {
        var state = EnsureState(world);
        return state.Diplomacy.Relations.FirstOrDefault(relation => string.Equals(relation.Id, BuildRelationId(factionAId, factionBId), StringComparison.Ordinal));
    }

    internal static WarStateRuntime? FindWarState(SimulationWorld world, string factionAId, string factionBId) =>
      EnsureState(world).Diplomacy.Wars.FirstOrDefault(war => string.Equals(war.RelationId, BuildRelationId(factionAId, factionBId), StringComparison.Ordinal) && war.Status == "active");

    internal static TerritoryControlStateRuntime? GetSystemControlState(SimulationWorld world, string systemId) =>
      EnsureState(world).Territory.ControlStates.FirstOrDefault(state => string.Equals(state.SystemId, systemId, StringComparison.Ordinal));

    internal static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId) =>
      string.Equals(GetSystemControlState(world, systemId)?.ControllerFactionId, factionId, StringComparison.Ordinal);

    internal static IReadOnlyList<string> GetControlledSystems(SimulationWorld world, string factionId) =>
      EnsureState(world).Territory.ControlStates
        .Where(state => string.Equals(state.ControllerFactionId, factionId, StringComparison.Ordinal))
        .OrderBy(state => state.SystemId, StringComparer.Ordinal)
        .Select(state => state.SystemId)
        .ToList();

    internal static float GetSystemRouteRisk(SimulationWorld world, string systemId, string? factionId = null)
    {
        var pressure = EnsureState(world).Territory.Pressures
          .Where(entry => string.Equals(entry.SystemId, systemId, StringComparison.Ordinal)
            && (factionId is null || string.Equals(entry.FactionId, factionId, StringComparison.Ordinal)))
          .OrderByDescending(entry => entry.CorridorRisk)
          .ThenBy(entry => entry.Id, StringComparer.Ordinal)
          .FirstOrDefault();
        return pressure?.CorridorRisk
          ?? EnsureState(world).Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == systemId)?.TerritorialPressure
          ?? 0f;
    }

    internal static bool HasHostileRelation(SimulationWorld world, string factionAId, string factionBId)
    {
        if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
        {
            return false;
        }

        var relation = FindRelation(world, factionAId, factionBId);
        return relation is not null && relation.Posture is "hostile" or "war";
    }

    internal static bool HasTradeAccess(SimulationWorld world, string factionAId, string factionBId)
    {
        if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
        {
            return true;
        }

        var relation = FindRelation(world, factionAId, factionBId);
        return relation?.TradeAccessPolicy is "open" or "allied";
    }

    internal static bool HasMilitaryAccess(SimulationWorld world, string factionAId, string factionBId)
    {
        if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
        {
            return true;
        }

        var relation = FindRelation(world, factionAId, factionBId);
        return relation?.MilitaryAccessPolicy is "open" or "allied";
    }

    internal static EconomicRegionRuntime? GetPrimaryEconomicRegion(SimulationWorld world, string factionId, string systemId) =>
      EnsureState(world).EconomyRegions.Regions.FirstOrDefault(region =>
        string.Equals(region.FactionId, factionId, StringComparison.Ordinal)
        && region.SystemIds.Contains(systemId, StringComparer.Ordinal));

    private static void RebuildRoutes(SimulationWorld world, GeopoliticalStateRuntime state)
    {
        state.Routes.Clear();
        if (world.Systems.Count <= 1)
        {
            return;
        }

        var systems = world.Systems
          .OrderBy(system => system.Definition.Id, StringComparer.Ordinal)
          .ToList();
        var routeIds = new HashSet<string>(StringComparer.Ordinal);

        foreach (var system in systems)
        {
            foreach (var neighbor in systems
              .Where(candidate => candidate.Definition.Id != system.Definition.Id)
              .Select(candidate => new
              {
                  candidate.Definition.Id,
                  Distance = system.Position.DistanceTo(candidate.Position),
              })
              .OrderBy(candidate => candidate.Distance)
              .ThenBy(candidate => candidate.Id, StringComparer.Ordinal)
              .Take(Math.Min(3, systems.Count - 1)))
            {
                var routeId = BuildPairId("route", system.Definition.Id, neighbor.Id);
                if (!routeIds.Add(routeId))
                {
                    continue;
                }

                state.Routes.Add(new SystemRouteLinkRuntime
                {
                    Id = routeId,
                    SourceSystemId = string.Compare(system.Definition.Id, neighbor.Id, StringComparison.Ordinal) <= 0 ? system.Definition.Id : neighbor.Id,
                    DestinationSystemId = string.Compare(system.Definition.Id, neighbor.Id, StringComparison.Ordinal) <= 0 ? neighbor.Id : system.Definition.Id,
                    Distance = neighbor.Distance,
                    IsPrimaryLane = true,
                });
            }
        }
    }

    private static void RebuildTerritory(SimulationWorld world, GeopoliticalStateRuntime state)
    {
        state.Territory.Claims.Clear();
        state.Territory.Influences.Clear();
        state.Territory.ControlStates.Clear();
        state.Territory.StrategicProfiles.Clear();
        state.Territory.BorderEdges.Clear();
        state.Territory.FrontLines.Clear();
        state.Territory.Zones.Clear();
        state.Territory.Pressures.Clear();

        var nowUtc = world.GeneratedAtUtc;
        foreach (var claim in world.Claims.Where(claim => claim.State != ClaimStateKinds.Destroyed))
        {
            state.Territory.Claims.Add(new TerritoryClaimRuntime
            {
                Id = $"territory-{claim.Id}",
                SourceClaimId = claim.Id,
                FactionId = claim.FactionId,
                SystemId = claim.SystemId,
                CelestialId = claim.CelestialId,
                Status = claim.State,
                ClaimKind = "infrastructure",
                ClaimStrength = claim.State == ClaimStateKinds.Active ? 1f : 0.65f,
                UpdatedAtUtc = nowUtc,
            });
        }

        var influencesBySystem = new Dictionary<string, List<TerritoryInfluenceRuntime>>(StringComparer.Ordinal);
        foreach (var system in world.Systems)
        {
            var claimsByFaction = state.Territory.Claims
              .Where(claim => claim.SystemId == system.Definition.Id)
              .GroupBy(claim => claim.FactionId, StringComparer.Ordinal);
            var stationsByFaction = world.Stations
              .Where(station => station.SystemId == system.Definition.Id)
              .GroupBy(station => station.FactionId, StringComparer.Ordinal);
            var shipsByFaction = world.Ships
              .Where(ship => ship.SystemId == system.Definition.Id && ship.Health > 0f)
              .GroupBy(ship => ship.FactionId, StringComparer.Ordinal);
            var sitesByFaction = world.ConstructionSites
              .Where(site => site.SystemId == system.Definition.Id && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed)
              .GroupBy(site => site.FactionId, StringComparer.Ordinal);

            var factionIds = claimsByFaction.Select(group => group.Key)
              .Concat(stationsByFaction.Select(group => group.Key))
              .Concat(shipsByFaction.Select(group => group.Key))
              .Concat(sitesByFaction.Select(group => group.Key))
              .Distinct(StringComparer.Ordinal)
              .OrderBy(id => id, StringComparer.Ordinal)
              .ToList();

            var influences = new List<TerritoryInfluenceRuntime>();
            foreach (var factionId in factionIds)
            {
                var claimStrength = claimsByFaction.FirstOrDefault(group => group.Key == factionId)?.Sum(claim => claim.ClaimStrength * 40f) ?? 0f;
                var stationStrength = (stationsByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 50f;
                var siteStrength = (sitesByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 18f;
                var shipStrength = shipsByFaction.FirstOrDefault(group => group.Key == factionId)?.Sum(ship =>
                {
                    if (IsMilitaryShip(ship.Definition))
                    {
                        return 9f;
                    }

                    if (IsConstructionShip(ship.Definition))
                    {
                        return 4f;
                    }

                    if (IsTransportShip(ship.Definition) || IsMiningShip(ship.Definition))
                    {
                        return 3f;
                    }

                    return 2f;
                }) ?? 0f;
                var logisticsStrength = MathF.Min(30f, stationStrength * 0.18f) + siteStrength;
                influences.Add(new TerritoryInfluenceRuntime
                {
                    Id = $"influence-{system.Definition.Id}-{factionId}",
                    SystemId = system.Definition.Id,
                    FactionId = factionId,
                    ClaimStrength = claimStrength,
                    AssetStrength = stationStrength + shipStrength,
                    LogisticsStrength = logisticsStrength,
                    TotalInfluence = claimStrength + stationStrength + shipStrength + logisticsStrength,
                    UpdatedAtUtc = nowUtc,
                });
            }

            influences.Sort((left, right) =>
            {
                var total = right.TotalInfluence.CompareTo(left.TotalInfluence);
                return total != 0 ? total : string.Compare(left.FactionId, right.FactionId, StringComparison.Ordinal);
            });
            if (influences.Count > 1)
            {
                var lead = influences[0].TotalInfluence;
                foreach (var influence in influences.Skip(1))
                {
                    influence.IsContesting = influence.TotalInfluence >= (lead * 0.7f);
                }

                influences[0].IsContesting = influences[1].TotalInfluence >= (lead * 0.7f);
            }

            influencesBySystem[system.Definition.Id] = influences;
            state.Territory.Influences.AddRange(influences);

            var top = influences.FirstOrDefault();
            var second = influences.Skip(1).FirstOrDefault();
            var contested = top is not null && second is not null && second.TotalInfluence >= (top.TotalInfluence * 0.7f);
            var controllerFactionId = top is not null && (!contested || top.TotalInfluence >= second!.TotalInfluence + 20f)
              ? top.FactionId
              : null;
            var primaryClaimantFactionId = state.Territory.Claims
              .Where(claim => claim.SystemId == system.Definition.Id)
              .GroupBy(claim => claim.FactionId, StringComparer.Ordinal)
              .OrderByDescending(group => group.Sum(claim => claim.ClaimStrength))
              .ThenBy(group => group.Key, StringComparer.Ordinal)
              .Select(group => group.Key)
              .FirstOrDefault();

            var strategicValue = EstimateSystemStrategicValue(world, system.Definition.Id);
            var controlState = new TerritoryControlStateRuntime
            {
                SystemId = system.Definition.Id,
                ControllerFactionId = controllerFactionId,
                PrimaryClaimantFactionId = primaryClaimantFactionId,
                ControlKind = contested
                ? "contested"
                : controllerFactionId is not null
                  ? "controlled"
                  : primaryClaimantFactionId is not null
                    ? "claimed"
                    : "unclaimed",
                IsContested = contested,
                ControlScore = top?.TotalInfluence ?? 0f,
                StrategicValue = strategicValue,
                UpdatedAtUtc = nowUtc,
            };
            controlState.ClaimantFactionIds.AddRange(state.Territory.Claims
              .Where(claim => claim.SystemId == system.Definition.Id)
              .Select(claim => claim.FactionId)
              .Distinct(StringComparer.Ordinal)
              .OrderBy(id => id, StringComparer.Ordinal));
            controlState.InfluencingFactionIds.AddRange(influences
              .Select(influence => influence.FactionId)
              .OrderBy(id => id, StringComparer.Ordinal));
            state.Territory.ControlStates.Add(controlState);
        }

        foreach (var route in state.Routes)
        {
            var left = state.Territory.ControlStates.First(stateItem => stateItem.SystemId == route.SourceSystemId);
            var right = state.Territory.ControlStates.First(stateItem => stateItem.SystemId == route.DestinationSystemId);
            var differentControllers = !string.Equals(left.ControllerFactionId, right.ControllerFactionId, StringComparison.Ordinal);
            var contested = left.IsContested || right.IsContested || differentControllers;
            if (!contested && left.ControllerFactionId is null && right.ControllerFactionId is null)
            {
                continue;
            }

            state.Territory.BorderEdges.Add(new BorderEdgeRuntime
            {
                Id = $"border-{route.Id}",
                SourceSystemId = route.SourceSystemId,
                DestinationSystemId = route.DestinationSystemId,
                SourceFactionId = left.ControllerFactionId ?? left.PrimaryClaimantFactionId,
                DestinationFactionId = right.ControllerFactionId ?? right.PrimaryClaimantFactionId,
                IsContested = contested,
                TensionScore = MathF.Min(1f, MathF.Abs((left.ControlScore - right.ControlScore) / MathF.Max(50f, left.ControlScore + right.ControlScore))),
                CorridorImportance = route.Distance <= 0.01f ? 0f : Math.Clamp((left.StrategicValue + right.StrategicValue) / MathF.Max(route.Distance, 1f), 0f, 1f),
                UpdatedAtUtc = nowUtc,
            });
        }

        foreach (var control in state.Territory.ControlStates)
        {
            var adjacentBorders = state.Territory.BorderEdges.Where(edge => edge.SourceSystemId == control.SystemId || edge.DestinationSystemId == control.SystemId).ToList();
            var hostileBorderCount = adjacentBorders.Count(edge => edge.IsContested);
            var corridorImportance = adjacentBorders.Sum(edge => edge.CorridorImportance);
            var zoneKind = control.IsContested
              ? "contested"
              : control.ControllerFactionId is null && control.PrimaryClaimantFactionId is not null
                ? "buffer"
                : control.ControllerFactionId is not null && hostileBorderCount == 0
                  ? "core"
                  : control.ControllerFactionId is not null && corridorImportance > 1.1f
                    ? "corridor"
                    : control.ControllerFactionId is not null
                      ? "frontier"
                      : "unclaimed";
            state.Territory.Zones.Add(new TerritoryZoneRuntime
            {
                Id = $"zone-{control.SystemId}",
                SystemId = control.SystemId,
                FactionId = control.ControllerFactionId ?? control.PrimaryClaimantFactionId,
                Kind = zoneKind,
                Status = "active",
                Reason = zoneKind == "corridor" ? "high-corridor-importance" : zoneKind == "frontier" ? "hostile-border-contact" : zoneKind,
                UpdatedAtUtc = nowUtc,
            });
            state.Territory.StrategicProfiles.Add(new SectorStrategicProfileRuntime
            {
                SystemId = control.SystemId,
                ControllerFactionId = control.ControllerFactionId,
                ZoneKind = zoneKind,
                IsContested = control.IsContested,
                StrategicValue = control.StrategicValue,
                SecurityRating = Math.Clamp(1f - (hostileBorderCount * 0.22f), 0f, 1f),
                TerritorialPressure = Math.Clamp(hostileBorderCount * 0.25f, 0f, 1f),
                LogisticsValue = Math.Clamp(corridorImportance, 0f, 1f),
                UpdatedAtUtc = nowUtc,
            });
            state.Territory.Pressures.Add(new TerritoryPressureRuntime
            {
                Id = $"pressure-{control.SystemId}",
                SystemId = control.SystemId,
                FactionId = control.ControllerFactionId ?? control.PrimaryClaimantFactionId,
                Kind = control.IsContested ? "contested-pressure" : "territorial-pressure",
                PressureScore = Math.Clamp(hostileBorderCount * 0.28f, 0f, 1f),
                SecurityScore = Math.Clamp(1f - (hostileBorderCount * 0.2f), 0f, 1f),
                HostileInfluence = influencesBySystem.GetValueOrDefault(control.SystemId)?.Skip(control.ControllerFactionId is null ? 0 : 1).Sum(entry => entry.TotalInfluence) ?? 0f,
                CorridorRisk = Math.Clamp(corridorImportance > 0.8f && hostileBorderCount > 0 ? 0.7f : hostileBorderCount * 0.2f, 0f, 1f),
                UpdatedAtUtc = nowUtc,
            });
        }
    }

    private static void RebuildDiplomacy(SimulationWorld world, GeopoliticalStateRuntime state, ICollection<SimulationEventRecord> events)
    {
        state.Diplomacy.Relations.Clear();
        state.Diplomacy.Treaties.Clear();
        state.Diplomacy.BorderTensions.Clear();
        state.Diplomacy.Wars.Clear();

        var nowUtc = world.GeneratedAtUtc;
        var factionPairs = world.Factions
          .OrderBy(faction => faction.Id, StringComparer.Ordinal)
          .SelectMany((left, index) => world.Factions.Skip(index + 1).Select(right => (left, right)));

        foreach (var (leftFaction, rightFaction) in factionPairs)
        {
            var borderEdges = state.Territory.BorderEdges
              .Where(edge =>
                (string.Equals(edge.SourceFactionId, leftFaction.Id, StringComparison.Ordinal) && string.Equals(edge.DestinationFactionId, rightFaction.Id, StringComparison.Ordinal))
                || (string.Equals(edge.SourceFactionId, rightFaction.Id, StringComparison.Ordinal) && string.Equals(edge.DestinationFactionId, leftFaction.Id, StringComparison.Ordinal)))
              .OrderBy(edge => edge.Id, StringComparer.Ordinal)
              .ToList();
            var sharedBorderPressure = borderEdges.Sum(edge => edge.TensionScore + (edge.IsContested ? 0.25f : 0f));
            var conflictSystems = borderEdges.SelectMany(edge => new[] { edge.SourceSystemId, edge.DestinationSystemId }).Distinct(StringComparer.Ordinal).ToList();
            var hostilePresence = world.Ships.Count(ship =>
              ship.Health > 0f
              && ((ship.FactionId == leftFaction.Id && conflictSystems.Contains(ship.SystemId, StringComparer.Ordinal))
                || (ship.FactionId == rightFaction.Id && conflictSystems.Contains(ship.SystemId, StringComparer.Ordinal))));
            var incidentSeverity = Math.Clamp(sharedBorderPressure + (hostilePresence * 0.03f), 0f, 1.6f);
            var relationId = BuildRelationId(leftFaction.Id, rightFaction.Id);
            var posture = incidentSeverity switch
            {
                >= 1.1f => "war",
                >= 0.65f => "hostile",
                >= 0.3f => "wary",
                _ => "neutral",
            };

            var relation = new DiplomaticRelationRuntime
            {
                Id = relationId,
                FactionAId = leftFaction.Id,
                FactionBId = rightFaction.Id,
                Status = "active",
                Posture = posture,
                TrustScore = Math.Clamp(0.7f - incidentSeverity, 0f, 1f),
                TensionScore = Math.Clamp(incidentSeverity, 0f, 1f),
                GrievanceScore = Math.Clamp(sharedBorderPressure, 0f, 1f),
                TradeAccessPolicy = posture is "war" or "hostile" ? "restricted" : "open",
                MilitaryAccessPolicy = posture == "neutral" ? "transit" : posture == "wary" ? "restricted" : "denied",
                UpdatedAtUtc = nowUtc,
            };

            if (relation.Posture == "neutral")
            {
                var treaty = new TreatyRuntime
                {
                    Id = $"treaty-open-trade-{relationId}",
                    Kind = "trade-understanding",
                    Status = "active",
                    TradeAccessPolicy = "open",
                    MilitaryAccessPolicy = "restricted",
                    Summary = $"Open civilian trade between {leftFaction.Label} and {rightFaction.Label}.",
                    CreatedAtUtc = nowUtc,
                    UpdatedAtUtc = nowUtc,
                };
                treaty.FactionIds.Add(leftFaction.Id);
                treaty.FactionIds.Add(rightFaction.Id);
                state.Diplomacy.Treaties.Add(treaty);
                relation.ActiveTreatyIds.Add(treaty.Id);
                relation.TradeAccessPolicy = "open";
            }

            state.Diplomacy.Relations.Add(relation);

            foreach (var borderEdge in borderEdges)
            {
                borderEdge.RelationId = relation.Id;
                borderEdge.TensionScore = Math.Clamp(borderEdge.TensionScore + (relation.TensionScore * 0.35f), 0f, 1f);
                var tension = new BorderTensionRuntime
                {
                    Id = $"tension-{borderEdge.Id}",
                    RelationId = relation.Id,
                    BorderEdgeId = borderEdge.Id,
                    FactionAId = leftFaction.Id,
                    FactionBId = rightFaction.Id,
                    Status = relation.Posture is "war" or "hostile" ? "escalating" : "stable",
                    TensionScore = relation.TensionScore,
                    IncidentScore = incidentSeverity,
                    MilitaryPressure = Math.Clamp(hostilePresence * 0.05f, 0f, 1f),
                    AccessFriction = relation.TradeAccessPolicy == "open" ? 0.15f : 0.75f,
                    UpdatedAtUtc = nowUtc,
                };
                tension.SystemIds.Add(borderEdge.SourceSystemId);
                tension.SystemIds.Add(borderEdge.DestinationSystemId);
                state.Diplomacy.BorderTensions.Add(tension);

                if (tension.TensionScore >= 0.35f)
                {
                    var incidentId = $"incident-border-{relationId}-{borderEdge.Id}";
                    var incident = new DiplomaticIncidentRuntime
                    {
                        Id = incidentId,
                        Kind = borderEdge.IsContested ? "border-clash" : "border-friction",
                        Status = relation.Posture == "war" ? "escalated" : "active",
                        SourceFactionId = leftFaction.Id,
                        TargetFactionId = rightFaction.Id,
                        SystemId = borderEdge.SourceSystemId,
                        BorderEdgeId = borderEdge.Id,
                        Summary = $"{leftFaction.Label} and {rightFaction.Label} are under pressure on {borderEdge.SourceSystemId}/{borderEdge.DestinationSystemId}.",
                        Severity = tension.TensionScore,
                        EscalationScore = tension.IncidentScore,
                        CreatedAtUtc = nowUtc,
                        LastObservedAtUtc = nowUtc,
                    };
                    state.Diplomacy.Incidents.Add(incident);
                    relation.ActiveIncidentIds.Add(incident.Id);
                }
            }

            if (relation.Posture == "war")
            {
                var warId = $"war-{relationId}";
                var war = new WarStateRuntime
                {
                    Id = warId,
                    RelationId = relation.Id,
                    FactionAId = leftFaction.Id,
                    FactionBId = rightFaction.Id,
                    Status = "active",
                    WarGoal = "border-dominance",
                    EscalationScore = relation.TensionScore,
                    StartedAtUtc = nowUtc,
                    UpdatedAtUtc = nowUtc,
                };
                relation.WarStateId = war.Id;
                state.Diplomacy.Wars.Add(war);
            }
        }

        BuildFrontLines(state, nowUtc, events);
    }

    private static void BuildFrontLines(GeopoliticalStateRuntime state, DateTimeOffset nowUtc, ICollection<SimulationEventRecord> events)
    {
        foreach (var group in state.Diplomacy.BorderTensions
          .Where(tension => tension.TensionScore >= 0.35f)
          .GroupBy(tension => BuildPairId("front", tension.FactionAId, tension.FactionBId), StringComparer.Ordinal))
        {
            var tensions = group.OrderByDescending(tension => tension.TensionScore).ThenBy(tension => tension.Id, StringComparer.Ordinal).ToList();
            var front = new FrontLineRuntime
            {
                Id = group.Key,
                Kind = state.Diplomacy.Wars.Any(war => war.RelationId == tensions[0].RelationId && war.Status == "active") ? "war-front" : "border-front",
                Status = "active",
                AnchorSystemId = tensions.SelectMany(tension => tension.SystemIds).GroupBy(systemId => systemId, StringComparer.Ordinal).OrderByDescending(entry => entry.Count()).ThenBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => entry.Key).FirstOrDefault(),
                PressureScore = Math.Clamp(tensions.Sum(tension => tension.TensionScore) / tensions.Count, 0f, 1f),
                SupplyRisk = Math.Clamp(tensions.Sum(tension => tension.AccessFriction) / tensions.Count, 0f, 1f),
                UpdatedAtUtc = nowUtc,
            };
            front.FactionIds.Add(tensions[0].FactionAId);
            front.FactionIds.Add(tensions[0].FactionBId);
            front.SystemIds.AddRange(tensions.SelectMany(tension => tension.SystemIds).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal));
            front.BorderEdgeIds.AddRange(tensions.Select(tension => tension.BorderEdgeId).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal));
            state.Territory.FrontLines.Add(front);

            foreach (var war in state.Diplomacy.Wars.Where(war => string.Equals(war.RelationId, tensions[0].RelationId, StringComparison.Ordinal)))
            {
                war.ActiveFrontLineIds.Add(front.Id);
            }

            events.Add(new SimulationEventRecord("front-line", front.Id, "front-updated", $"Front {front.Id} pressure {front.PressureScore.ToString("0.00", CultureInfo.InvariantCulture)}.", nowUtc, "geopolitics"));
        }

        foreach (var profile in state.Territory.StrategicProfiles)
        {
            profile.FrontLineId = state.Territory.FrontLines.FirstOrDefault(front => front.SystemIds.Contains(profile.SystemId, StringComparer.Ordinal))?.Id;
        }
    }

    private static void RebuildEconomyRegions(SimulationWorld world, GeopoliticalStateRuntime state)
    {
        state.EconomyRegions.Regions.Clear();
        state.EconomyRegions.SupplyNetworks.Clear();
        state.EconomyRegions.Corridors.Clear();
        state.EconomyRegions.ProductionProfiles.Clear();
        state.EconomyRegions.TradeBalances.Clear();
        state.EconomyRegions.Bottlenecks.Clear();
        state.EconomyRegions.SecurityAssessments.Clear();
        state.EconomyRegions.EconomicAssessments.Clear();

        var nowUtc = world.GeneratedAtUtc;
        foreach (var faction in world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal))
        {
            var factionSystems = state.Territory.ControlStates
              .Where(control => string.Equals(control.ControllerFactionId ?? control.PrimaryClaimantFactionId, faction.Id, StringComparison.Ordinal))
              .Select(control => control.SystemId)
              .Distinct(StringComparer.Ordinal)
              .OrderBy(systemId => systemId, StringComparer.Ordinal)
              .ToList();
            if (factionSystems.Count == 0)
            {
                continue;
            }

            var connectedComponents = BuildConnectedComponents(factionSystems, state.Routes);
            foreach (var component in connectedComponents)
            {
                var coreSystemId = component
                  .OrderByDescending(systemId => world.Stations.Count(station => station.FactionId == faction.Id && station.SystemId == systemId))
                  .ThenBy(systemId => systemId, StringComparer.Ordinal)
                  .First();
                var regionId = $"region-{faction.Id}-{coreSystemId}";
                var stations = world.Stations
                  .Where(station => station.FactionId == faction.Id && component.Contains(station.SystemId, StringComparer.Ordinal))
                  .OrderBy(station => station.Id, StringComparer.Ordinal)
                  .ToList();
                var economy = BuildRegionalEconomy(world, faction.Id, component);
                var regionKind = ResolveRegionKind(stations, economy);
                var frontLineIds = state.Territory.FrontLines
                  .Where(front => front.SystemIds.Any(systemId => component.Contains(systemId, StringComparer.Ordinal)))
                  .Select(front => front.Id)
                  .Distinct(StringComparer.Ordinal)
                  .OrderBy(id => id, StringComparer.Ordinal)
                  .ToList();

                var region = new EconomicRegionRuntime
                {
                    Id = regionId,
                    FactionId = faction.Id,
                    Label = $"{faction.Label} {coreSystemId}",
                    Kind = regionKind,
                    Status = "active",
                    CoreSystemId = coreSystemId,
                    UpdatedAtUtc = nowUtc,
                };
                region.SystemIds.AddRange(component.OrderBy(id => id, StringComparer.Ordinal));
                region.StationIds.AddRange(stations.Select(station => station.Id));
                region.FrontLineIds.AddRange(frontLineIds);
                state.EconomyRegions.Regions.Add(region);

                var producerItems = economy.Commodities
                  .Where(entry => entry.Value.ProductionRatePerSecond > 0.01f)
                  .OrderByDescending(entry => entry.Value.ProductionRatePerSecond)
                  .ThenBy(entry => entry.Key, StringComparer.Ordinal)
                  .Take(8)
                  .Select(entry => entry.Key)
                  .ToList();
                var scarceItems = economy.Commodities
                  .Where(entry => entry.Value.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low)
                  .OrderByDescending(entry => CommodityOperationalSignal.ComputeNeedScore(entry.Value, 240f))
                  .ThenBy(entry => entry.Key, StringComparer.Ordinal)
                  .Take(8)
                  .Select(entry => entry.Key)
                  .ToList();

                var supplyNetwork = new SupplyNetworkRuntime
                {
                    Id = $"network-{regionId}",
                    RegionId = regionId,
                    ThroughputScore = Math.Clamp(stations.Count * 0.18f, 0f, 1f),
                    RiskScore = Math.Clamp(frontLineIds.Count * 0.24f, 0f, 1f),
                    UpdatedAtUtc = nowUtc,
                };
                supplyNetwork.StationIds.AddRange(stations.Select(station => station.Id));
                supplyNetwork.ProducerItemIds.AddRange(producerItems);
                supplyNetwork.ConsumerItemIds.AddRange(scarceItems);
                supplyNetwork.ConstructionItemIds.AddRange(world.ConstructionSites
                  .Where(site => site.FactionId == faction.Id && component.Contains(site.SystemId, StringComparer.Ordinal))
                  .SelectMany(site => site.RequiredItems.Keys)
                  .Distinct(StringComparer.Ordinal)
                  .OrderBy(id => id, StringComparer.Ordinal));
                state.EconomyRegions.SupplyNetworks.Add(supplyNetwork);

                var productionProfile = new RegionalProductionProfileRuntime
                {
                    RegionId = regionId,
                    PrimaryIndustry = regionKind,
                    ShipyardCount = stations.Count(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
                    StationCount = stations.Count,
                    UpdatedAtUtc = nowUtc,
                };
                productionProfile.ProducedItemIds.AddRange(producerItems);
                productionProfile.ScarceItemIds.AddRange(scarceItems);
                state.EconomyRegions.ProductionProfiles.Add(productionProfile);

                state.EconomyRegions.TradeBalances.Add(new RegionalTradeBalanceRuntime
                {
                    RegionId = regionId,
                    ImportsRequiredCount = economy.Commodities.Count(entry => entry.Value.BuyBacklog > 0.01f),
                    ExportsSurplusCount = economy.Commodities.Count(entry => entry.Value.SellBacklog > 0.01f || entry.Value.Level == CommodityLevelKind.Surplus),
                    CriticalShortageCount = scarceItems.Count,
                    NetTradeScore = Math.Clamp((economy.Commodities.Sum(entry => entry.Value.ProjectedNetRatePerSecond) + 5f) / 10f, -1f, 1f),
                    UpdatedAtUtc = nowUtc,
                });

                if (scarceItems.FirstOrDefault() is { } bottleneckItemId)
                {
                    state.EconomyRegions.Bottlenecks.Add(new RegionalBottleneckRuntime
                    {
                        Id = $"bottleneck-{regionId}-{bottleneckItemId}",
                        RegionId = regionId,
                        ItemId = bottleneckItemId,
                        Cause = "regional-shortage",
                        Status = "active",
                        Severity = Math.Clamp(CommodityOperationalSignal.ComputeNeedScore(economy.GetCommodity(bottleneckItemId), 240f), 0f, 10f),
                        UpdatedAtUtc = nowUtc,
                    });
                }

                var supplyRisk = Math.Clamp(frontLineIds.Count * 0.2f, 0f, 1f);
                state.EconomyRegions.SecurityAssessments.Add(new RegionalSecurityAssessmentRuntime
                {
                    RegionId = regionId,
                    SupplyRisk = supplyRisk,
                    BorderPressure = Math.Clamp(frontLineIds.Count * 0.22f, 0f, 1f),
                    ActiveWarCount = state.Diplomacy.Wars.Count(war => war.ActiveFrontLineIds.Intersect(frontLineIds, StringComparer.Ordinal).Any()),
                    HostileRelationCount = state.Diplomacy.Relations.Count(relation => relation.Posture is "hostile" or "war"),
                    AccessFriction = Math.Clamp(state.Diplomacy.BorderTensions.Where(tension => tension.SystemIds.Any(systemId => component.Contains(systemId, StringComparer.Ordinal))).DefaultIfEmpty().Average(tension => tension?.AccessFriction ?? 0f), 0f, 1f),
                    UpdatedAtUtc = nowUtc,
                });

                state.EconomyRegions.EconomicAssessments.Add(new RegionalEconomicAssessmentRuntime
                {
                    RegionId = regionId,
                    SustainmentScore = Math.Clamp(1f - (scarceItems.Count * 0.12f) - (supplyRisk * 0.35f), 0f, 1f),
                    ProductionDepth = Math.Clamp(producerItems.Count / 8f, 0f, 1f),
                    ConstructionPressure = Math.Clamp(world.ConstructionSites.Count(site => site.FactionId == faction.Id && component.Contains(site.SystemId, StringComparer.Ordinal)) * 0.22f, 0f, 1f),
                    CorridorDependency = Math.Clamp(frontLineIds.Count * 0.18f, 0f, 1f),
                    UpdatedAtUtc = nowUtc,
                });
            }
        }

        BuildCorridors(world, state, nowUtc);
        foreach (var profile in state.Territory.StrategicProfiles)
        {
            profile.EconomicRegionId = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(profile.SystemId, StringComparer.Ordinal))?.Id;
        }
    }

    private static void BuildCorridors(SimulationWorld world, GeopoliticalStateRuntime state, DateTimeOffset nowUtc)
    {
        foreach (var route in state.Routes)
        {
            var sourceRegion = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(route.SourceSystemId, StringComparer.Ordinal));
            var destinationRegion = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(route.DestinationSystemId, StringComparer.Ordinal));
            if (sourceRegion is null && destinationRegion is null)
            {
                continue;
            }

            var borderEdge = state.Territory.BorderEdges.FirstOrDefault(edge =>
              (edge.SourceSystemId == route.SourceSystemId && edge.DestinationSystemId == route.DestinationSystemId)
              || (edge.SourceSystemId == route.DestinationSystemId && edge.DestinationSystemId == route.SourceSystemId));
            var risk = borderEdge?.TensionScore ?? 0f;
            var corridor = new LogisticsCorridorRuntime
            {
                Id = $"corridor-{route.Id}",
                FactionId = sourceRegion?.FactionId ?? destinationRegion?.FactionId,
                Kind = borderEdge?.IsContested == true ? "frontier-corridor" : "supply-corridor",
                Status = borderEdge?.IsContested == true ? "risky" : "active",
                RiskScore = Math.Clamp(risk + ((sourceRegion is not null && destinationRegion is not null && sourceRegion.Id != destinationRegion.Id) ? 0.15f : 0f), 0f, 1f),
                ThroughputScore = Math.Clamp(((sourceRegion?.StationIds.Count ?? 0) + (destinationRegion?.StationIds.Count ?? 0)) / 10f, 0f, 1f),
                AccessState = ResolveCorridorAccessState(world, borderEdge, sourceRegion, destinationRegion),
                UpdatedAtUtc = nowUtc,
            };
            corridor.SystemPathIds.Add(route.SourceSystemId);
            corridor.SystemPathIds.Add(route.DestinationSystemId);
            if (sourceRegion is not null)
            {
                corridor.RegionIds.Add(sourceRegion.Id);
            }
            if (destinationRegion is not null && !corridor.RegionIds.Contains(destinationRegion.Id, StringComparer.Ordinal))
            {
                corridor.RegionIds.Add(destinationRegion.Id);
            }
            if (borderEdge is not null)
            {
                corridor.BorderEdgeIds.Add(borderEdge.Id);
            }

            state.EconomyRegions.Corridors.Add(corridor);
            if (sourceRegion is not null && !sourceRegion.CorridorIds.Contains(corridor.Id, StringComparer.Ordinal))
            {
                sourceRegion.CorridorIds.Add(corridor.Id);
            }
            if (destinationRegion is not null && !destinationRegion.CorridorIds.Contains(corridor.Id, StringComparer.Ordinal))
            {
                destinationRegion.CorridorIds.Add(corridor.Id);
            }
        }
    }

    private static string ResolveCorridorAccessState(
      SimulationWorld world,
      BorderEdgeRuntime? borderEdge,
      EconomicRegionRuntime? sourceRegion,
      EconomicRegionRuntime? destinationRegion)
    {
        if (sourceRegion?.FactionId is null || destinationRegion?.FactionId is null)
        {
            return borderEdge?.IsContested == true ? "restricted" : "open";
        }

        var relation = FindRelation(world, sourceRegion.FactionId, destinationRegion.FactionId);
        if (relation is null)
        {
            return "restricted";
        }

        return relation.Posture switch
        {
            "war" => "denied",
            "hostile" => "restricted",
            _ => relation.TradeAccessPolicy,
        };
    }

    private static FactionEconomySnapshot BuildRegionalEconomy(SimulationWorld world, string factionId, IReadOnlyCollection<string> systemIds)
    {
        var snapshot = new FactionEconomySnapshot();
        foreach (var station in world.Stations.Where(station => station.FactionId == factionId && systemIds.Contains(station.SystemId, StringComparer.Ordinal)))
        {
            foreach (var (itemId, amount) in station.Inventory)
            {
                snapshot.GetCommodity(itemId).OnHand += amount;
            }

            foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station))
            {
                var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey);
                if (recipe is null)
                {
                    continue;
                }

                var throughput = StationSimulationService.GetStationProductionThroughput(world, station, recipe);
                var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f);
                foreach (var input in recipe.Inputs)
                {
                    snapshot.GetCommodity(input.ItemId).ConsumptionRatePerSecond += input.Amount * cyclesPerSecond;
                }
                foreach (var output in recipe.Outputs)
                {
                    snapshot.GetCommodity(output.ItemId).ProductionRatePerSecond += output.Amount * cyclesPerSecond;
                }
            }
        }

        foreach (var order in world.MarketOrders.Where(order => order.FactionId == factionId))
        {
            var relatedSystemId = world.Stations.FirstOrDefault(station => station.Id == order.StationId)?.SystemId
              ?? world.ConstructionSites.FirstOrDefault(site => site.Id == order.ConstructionSiteId)?.SystemId;
            if (relatedSystemId is null || !systemIds.Contains(relatedSystemId, StringComparer.Ordinal))
            {
                continue;
            }

            var commodity = snapshot.GetCommodity(order.ItemId);
            if (order.Kind == MarketOrderKinds.Buy)
            {
                commodity.BuyBacklog += order.RemainingAmount;
            }
            else if (order.Kind == MarketOrderKinds.Sell)
            {
                commodity.SellBacklog += order.RemainingAmount;
            }
        }

        foreach (var site in world.ConstructionSites.Where(site => site.FactionId == factionId && systemIds.Contains(site.SystemId, StringComparer.Ordinal)))
        {
            foreach (var required in site.RequiredItems)
            {
                var remaining = MathF.Max(0f, required.Value - (site.DeliveredItems.TryGetValue(required.Key, out var delivered) ? delivered : 0f));
                if (remaining > 0.01f)
                {
                    snapshot.GetCommodity(required.Key).ReservedForConstruction += remaining;
                }
            }
        }

        return snapshot;
    }

    private static List<List<string>> BuildConnectedComponents(IReadOnlyCollection<string> systems, IReadOnlyCollection<SystemRouteLinkRuntime> routes)
    {
        var remaining = systems.ToHashSet(StringComparer.Ordinal);
        var adjacency = routes
          .SelectMany(route => new[]
          {
        (route.SourceSystemId, route.DestinationSystemId),
        (route.DestinationSystemId, route.SourceSystemId),
          })
          .GroupBy(entry => entry.Item1, StringComparer.Ordinal)
          .ToDictionary(group => group.Key, group => group.Select(entry => entry.Item2).ToList(), StringComparer.Ordinal);
        var components = new List<List<string>>();

        while (remaining.Count > 0)
        {
            var start = remaining.OrderBy(id => id, StringComparer.Ordinal).First();
            var frontier = new Queue<string>();
            frontier.Enqueue(start);
            remaining.Remove(start);
            var component = new List<string>();

            while (frontier.Count > 0)
            {
                var current = frontier.Dequeue();
                component.Add(current);
                foreach (var neighbor in adjacency.GetValueOrDefault(current, []))
                {
                    if (remaining.Remove(neighbor))
                    {
                        frontier.Enqueue(neighbor);
                    }
                }
            }

            components.Add(component);
        }

        return components;
    }

    private static string ResolveRegionKind(IReadOnlyCollection<StationRuntime> stations, FactionEconomySnapshot economy)
    {
        if (stations.Any(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)))
        {
            return "shipbuilding-region";
        }

        if (stations.Count(station => StationSimulationService.DetermineStationRole(station) == "refinery") >= 2)
        {
            return "industrial-core";
        }

        if (economy.Commodities.Any(entry => entry.Value.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low))
        {
            return "frontier-sustainment";
        }

        return stations.Count <= 2 ? "extraction-region" : "balanced-region";
    }

    private static float EstimateSystemStrategicValue(SimulationWorld world, string systemId)
    {
        var stationValue = world.Stations.Count(station => station.SystemId == systemId) * 30f;
        var constructionValue = world.ConstructionSites.Count(site => site.SystemId == systemId && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed) * 18f;
        var nodeValue = world.Nodes.Count(node => node.SystemId == systemId) * 8f;
        return stationValue + constructionValue + nodeValue;
    }

    private static string BuildRelationId(string factionAId, string factionBId) =>
      BuildPairId("relation", factionAId, factionBId);

    private static string BuildPairId(string prefix, string leftId, string rightId)
    {
        return string.Compare(leftId, rightId, StringComparison.Ordinal) <= 0
          ? $"{prefix}-{leftId}-{rightId}"
          : $"{prefix}-{rightId}-{leftId}";
    }
}
