using static SpaceGame.Api.Universe.Scenario.LoaderSupport;

namespace SpaceGame.Api.Universe.Scenario;

public sealed class SpatialBuilder(IBalanceService balance)
{
    internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems)
    {
        var systemGraphs = systems.ToDictionary(
          system => system.Definition.Id,
          BuildSystemSpatialGraph,
          StringComparer.Ordinal);
        var celestials = systemGraphs.Values.SelectMany(graph => graph.Celestials).ToList();
        var nodes = new List<ResourceNodeRuntime>();
        var nodeIdCounter = 0;

        foreach (var system in systems)
        {
            var systemGraph = systemGraphs[system.Definition.Id];
            foreach (var node in system.Definition.ResourceNodes)
            {
                var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node);
                nodes.Add(new ResourceNodeRuntime
                {
                    Id = $"node-{++nodeIdCounter}",
                    SystemId = system.Definition.Id,
                    Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane),
                    SourceKind = node.SourceKind,
                    ItemId = node.ItemId,
                    CelestialId = anchorCelestial?.Id,
                    OrbitRadius = node.RadiusOffset,
                    OrbitPhase = node.Angle,
                    OrbitInclination = DegreesToRadians(node.InclinationDegrees),
                    OreRemaining = node.OreAmount,
                    MaxOre = node.OreAmount,
                });
            }
        }

        return new ScenarioSpatialLayout(systemGraphs, celestials, nodes);
    }

    private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
    {
        var celestials = new List<CelestialRuntime>();
        var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();

        for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
        {
            AddCelestial(
              celestials,
              id: $"node-{system.Definition.Id}-star-{starIndex + 1}",
              systemId: system.Definition.Id,
              kind: SpatialNodeKind.Star,
              position: Vector3.Zero,
              localSpaceRadius: LocalSpaceRadius);
        }

        var primaryStarNodeId = $"node-{system.Definition.Id}-star-1";

        for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
        {
            var planet = system.Definition.Planets[planetIndex];
            var planetNodeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}";
            var planetPosition = ComputePlanetPosition(planet);
            var planetCelestial = AddCelestial(
              celestials,
              id: planetNodeId,
              systemId: system.Definition.Id,
              kind: SpatialNodeKind.Planet,
              position: planetPosition,
              localSpaceRadius: LocalSpaceRadius,
              parentNodeId: primaryStarNodeId);

            var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
            foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
            {
                var lagrangeCelestial = AddCelestial(
                  celestials,
                  id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}",
                  systemId: system.Definition.Id,
                  kind: SpatialNodeKind.LagrangePoint,
                  position: point.Position,
                  localSpaceRadius: LocalSpaceRadius,
                  parentNodeId: planetCelestial.Id,
                  orbitReferenceId: point.Designation);
                lagrangeNodes[point.Designation] = lagrangeCelestial;
            }

            lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;

            for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
            {
                var moon = planet.Moons[moonIndex];
                var moonPosition = ComputeMoonPosition(planetPosition, moon);
                AddCelestial(
                  celestials,
                  id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
                  systemId: system.Definition.Id,
                  kind: SpatialNodeKind.Moon,
                  position: moonPosition,
                  localSpaceRadius: LocalSpaceRadius,
                  parentNodeId: planetCelestial.Id);
            }
        }

        return new SystemSpatialGraph(system.Definition.Id, celestials, lagrangeNodesByPlanetIndex);
    }

    private static CelestialRuntime AddCelestial(
      ICollection<CelestialRuntime> celestials,
      string id,
      string systemId,
      SpatialNodeKind kind,
      Vector3 position,
      float localSpaceRadius,
      string? parentNodeId = null,
      string? orbitReferenceId = null)
    {
        var celestial = new CelestialRuntime
        {
            Id = id,
            SystemId = systemId,
            Kind = kind,
            Position = position,
            LocalSpaceRadius = localSpaceRadius,
            ParentNodeId = parentNodeId,
            OrbitReferenceId = orbitReferenceId,
        };

        celestials.Add(celestial);
        return celestial;
    }

    private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(Vector3 planetPosition, PlanetDefinition planet)
    {
        var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
        var tangential = new Vector3(-radial.Z, 0f, radial.X);
        var orbitRadiusKm = MathF.Sqrt(planetPosition.X * planetPosition.X + planetPosition.Z * planetPosition.Z);
        var offset = ComputePlanetLocalLagrangeOffset(orbitRadiusKm, planet);
        var triangularAngle = MathF.PI / 3f;

        yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
        yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
        yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadiusKm));
        yield return new LagrangePointPlacement(
          "L4",
          Add(
            Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
            Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle))));
        yield return new LagrangePointPlacement(
          "L5",
          Add(
            Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
            Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle))));
    }

    private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet)
    {
        var planetMassProxy = EstimatePlanetMassRatio(planet);
        var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f));
        var minimumOffset = MathF.Max(planet.Size * 4f, 25000f);
        return MathF.Max(minimumOffset, hillLikeOffset);
    }

    private static float EstimatePlanetMassRatio(PlanetDefinition planet)
    {
        var earthRadiusRatio = MathF.Max(planet.Size / 6371f, 0.05f);
        var densityFactor = planet.PlanetType switch
        {
            "gas-giant" => 0.24f,
            "ice-giant" => 0.18f,
            "oceanic" => 0.95f,
            "ice" => 0.7f,
            _ => 1f,
        };

        var earthMasses = MathF.Pow(earthRadiusRatio, 3f) * densityFactor;
        return earthMasses / 332_946f;
    }

    internal static StationPlacement ResolveStationPlacement(
      InitialStationDefinition plan,
      SystemRuntime system,
      SystemSpatialGraph graph,
      IReadOnlyCollection<CelestialRuntime> existingCelestials)
    {
        if (plan.PlanetIndex is int planetIndex &&
            graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
        {
            var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
            if (lagrangeNodes.TryGetValue(designation, out var lagrangeCelestial))
            {
                return new StationPlacement(lagrangeCelestial, lagrangeCelestial.Position);
            }
        }

        if (plan.Position is { Length: 3 })
        {
            var targetPosition = NormalizeScenarioPoint(system, plan.Position);
            var preferredCelestial = existingCelestials
              .Where(c => c.SystemId == system.Definition.Id && c.Kind == SpatialNodeKind.LagrangePoint)
              .OrderBy(c => c.Position.DistanceTo(targetPosition))
              .FirstOrDefault()
              ?? existingCelestials
                .Where(c => c.SystemId == system.Definition.Id)
                .OrderBy(c => c.Position.DistanceTo(targetPosition))
                .First();
            return new StationPlacement(preferredCelestial, preferredCelestial.Position);
        }

        var fallbackCelestial = graph.Celestials
          .FirstOrDefault(c => c.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(c.OccupyingStructureId))
          ?? graph.Celestials.First(c => c.Kind == SpatialNodeKind.Planet);
        return new StationPlacement(fallbackCelestial, fallbackCelestial.Position);
    }

    private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch
    {
        < 0 => "L4",
        > 0 => "L5",
        _ => "L1",
    };

    private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
    {
        if (!string.IsNullOrWhiteSpace(definition.AnchorReference))
        {
            var anchorId = definition.AnchorReference.ToLowerInvariant() switch
            {
                var reference when reference.StartsWith("star-", StringComparison.Ordinal)
                  => $"node-{graph.SystemId}-{reference}",
                var reference when reference.StartsWith("planet-", StringComparison.Ordinal)
                  => $"node-{graph.SystemId}-{reference}",
                _ => null,
            };

            if (anchorId is not null)
            {
                return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal));
            }
        }

        if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0)
        {
            return null;
        }

        if (definition.AnchorMoonIndex is int moonIndex && moonIndex >= 0)
        {
            var moonNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
            return graph.Celestials.FirstOrDefault(c => c.Id == moonNodeId);
        }

        var planetNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}";
        return graph.Celestials.FirstOrDefault(c => c.Id == planetNodeId);
    }

    private static Vector3 ComputeResourceNodePosition(CelestialRuntime? anchorCelestial, ResourceNodeDefinition definition, float yPlane)
    {
        var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f);
        var offset = new Vector3(
          MathF.Cos(definition.Angle) * definition.RadiusOffset,
          verticalOffset,
          MathF.Sin(definition.Angle) * definition.RadiusOffset);

        if (anchorCelestial is null)
        {
            return new Vector3(offset.X, yPlane + offset.Y, offset.Z);
        }

        return Add(anchorCelestial.Position, offset);
    }

    private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
    {
        var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
        var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius);
        return new Vector3(MathF.Cos(angle) * orbitRadiusKm, 0f, MathF.Sin(angle) * orbitRadiusKm);
    }

    private static Vector3 ComputeMoonPosition(Vector3 planetPosition, MoonDefinition moon)
    {
        var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch);
        var local = new Vector3(MathF.Cos(angle) * moon.OrbitRadius, 0f, MathF.Sin(angle) * moon.OrbitRadius);
        return Add(planetPosition, local);
    }

    internal static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<CelestialRuntime> celestials)
    {
        var nearestCelestial = celestials
          .Where(c => c.SystemId == systemId)
          .OrderBy(c => c.Position.DistanceTo(position))
          .FirstOrDefault();

        return new ShipSpatialStateRuntime
        {
            CurrentSystemId = systemId,
            SpaceLayer = SpaceLayerKind.LocalSpace,
            CurrentCelestialId = nearestCelestial?.Id,
            LocalPosition = position,
            SystemPosition = position,
            MovementRegime = MovementRegimeKind.LocalFlight,
        };
    }
}

public sealed record ScenarioSpatialLayout(
  IReadOnlyDictionary<string, SystemSpatialGraph> SystemGraphs,
  List<CelestialRuntime> Celestials,
  List<ResourceNodeRuntime> Nodes);

public sealed record SystemSpatialGraph(
  string SystemId,
  List<CelestialRuntime> Celestials,
  Dictionary<int, Dictionary<string, CelestialRuntime>> LagrangeNodesByPlanetIndex);

internal sealed record LagrangePointPlacement(string Designation, Vector3 Position);

internal sealed record StationPlacement(CelestialRuntime AnchorCelestial, Vector3 Position);
