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

namespace SpaceGame.Api.Universe.Scenario;

public sealed class SystemGenerationService
{
    private const float KnownSystemSelectionChance = 0.5f;

    internal List<SolarSystemDefinition> PrepareKnownSystems(IReadOnlyList<SolarSystemDefinition> knownSystems) =>
      knownSystems
        .Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
        .ToList();

    internal List<SolarSystemDefinition> GenerateSystems(
      IReadOnlyList<SolarSystemDefinition> knownSystems,
      WorldGenerationOptions worldGenerationOptions)
    {
        if (worldGenerationOptions.TargetSystemCount <= 0)
        {
            return [];
        }

        if (knownSystems.Count == 0)
        {
            throw new InvalidOperationException("World generation requires at least one known system template.");
        }

        var systems = new List<SolarSystemDefinition>(worldGenerationOptions.TargetSystemCount);
        var availableKnownSystems = knownSystems.Select(CloneSystemDefinition).ToList();
        var templateSystems = knownSystems.Select(CloneSystemDefinition).ToList();

        var existingIds = new HashSet<string>(StringComparer.Ordinal);
        var occupiedPositions = new List<Vector3>();
        var generatedSystemCount = 0;

        for (var slotIndex = 0; slotIndex < worldGenerationOptions.TargetSystemCount; slotIndex += 1)
        {
            if (ShouldUseKnownSystem(worldGenerationOptions, slotIndex, availableKnownSystems.Count))
            {
                var knownSystemIndex = SelectKnownSystemIndex(worldGenerationOptions.Seed, slotIndex, availableKnownSystems.Count);
                var knownSystem = availableKnownSystems[knownSystemIndex];
                availableKnownSystems.RemoveAt(knownSystemIndex);
                systems.Add(knownSystem);
                existingIds.Add(knownSystem.Id);
                occupiedPositions.Add(ToVector(knownSystem.Position));
                continue;
            }

            var template = templateSystems[generatedSystemCount % templateSystems.Count];
            var name = GeneratedSystemNames[generatedSystemCount % GeneratedSystemNames.Length];
            var id = BuildGeneratedSystemId(name, generatedSystemCount + 1);
            while (!existingIds.Add(id))
            {
                id = $"{id}-x";
            }

            var position = BuildGeneratedSystemPosition(occupiedPositions, generatedSystemCount);
            systems.Add(CreateGeneratedSystem(template, name, id, generatedSystemCount, position));
            occupiedPositions.Add(position);
            generatedSystemCount += 1;
        }

        return systems;
    }

    private static SolarSystemDefinition CreateGeneratedSystem(
      SolarSystemDefinition template,
      string label,
      string id,
      int generatedIndex,
      Vector3 position)
    {
        var starProfile = SelectStarProfile(generatedIndex);
        var planets = BuildGeneratedPlanets(template, generatedIndex);
        var resourceNodes = BuildProceduralResourceNodes(template, planets, generatedIndex)
          .Select(node => new ResourceNodeDefinition
          {
              SourceKind = node.SourceKind,
              AnchorReference = node.AnchorReference,
              Angle = node.Angle,
              RadiusOffset = node.RadiusOffset,
              InclinationDegrees = node.InclinationDegrees,
              AnchorPlanetIndex = node.AnchorPlanetIndex,
              AnchorMoonIndex = node.AnchorMoonIndex,
              OreAmount = node.OreAmount,
              ItemId = node.ItemId,
              ShardCount = node.ShardCount,
          })
          .ToList();

        return EnsureStrategicResourceCoverage(new SolarSystemDefinition
        {
            Id = id,
            Label = label,
            Position = [position.X, position.Y, position.Z],
            Stars =
          [
            new StarDefinition
        {
          Kind = starProfile.Kind,
          Color = starProfile.StarColor,
          Glow = starProfile.StarGlow,
          Size = starProfile.BaseSize + ((generatedIndex % 4) * 2f),
        },
      ],
            AsteroidField = new AsteroidFieldDefinition
            {
                DecorationCount = template.AsteroidField.DecorationCount + ((generatedIndex % 5) * 10),
                RadiusOffset = template.AsteroidField.RadiusOffset + ((generatedIndex % 4) * 18000f),
                RadiusVariance = template.AsteroidField.RadiusVariance + ((generatedIndex % 3) * 12000f),
                HeightVariance = template.AsteroidField.HeightVariance + ((generatedIndex % 4) * 4000f),
            },
            ResourceNodes = resourceNodes,
            Planets = planets,
        }, generatedIndex + 1024);
    }

    private static SolarSystemDefinition CloneSystemDefinition(SolarSystemDefinition definition)
    {
        return new SolarSystemDefinition
        {
            Id = definition.Id,
            Label = definition.Label,
            Position = definition.Position.ToArray(),
            Stars = definition.Stars.Select(s => new StarDefinition { Kind = s.Kind, Color = s.Color, Glow = s.Glow, Size = s.Size, OrbitRadius = s.OrbitRadius, OrbitSpeed = s.OrbitSpeed, OrbitPhaseAtEpoch = s.OrbitPhaseAtEpoch }).ToList(),
            AsteroidField = new AsteroidFieldDefinition
            {
                DecorationCount = definition.AsteroidField.DecorationCount,
                RadiusOffset = definition.AsteroidField.RadiusOffset,
                RadiusVariance = definition.AsteroidField.RadiusVariance,
                HeightVariance = definition.AsteroidField.HeightVariance,
            },
            ResourceNodes = definition.ResourceNodes.Select(node => new ResourceNodeDefinition
            {
                SourceKind = node.SourceKind,
                AnchorReference = node.AnchorReference,
                Angle = node.Angle,
                RadiusOffset = node.RadiusOffset,
                InclinationDegrees = node.InclinationDegrees,
                AnchorPlanetIndex = node.AnchorPlanetIndex,
                AnchorMoonIndex = node.AnchorMoonIndex,
                OreAmount = node.OreAmount,
                ItemId = node.ItemId,
                ShardCount = node.ShardCount,
            }).ToList(),
            Planets = definition.Planets.Select(planet => new PlanetDefinition
            {
                Label = planet.Label,
                PlanetType = planet.PlanetType,
                Shape = planet.Shape,
                Moons = planet.Moons.Select(moon => new MoonDefinition { Label = moon.Label, Size = moon.Size, Color = moon.Color, OrbitRadius = moon.OrbitRadius, OrbitSpeed = moon.OrbitSpeed, OrbitPhaseAtEpoch = moon.OrbitPhaseAtEpoch, OrbitInclination = moon.OrbitInclination, OrbitLongitudeOfAscendingNode = moon.OrbitLongitudeOfAscendingNode }).ToList(),
                OrbitRadius = planet.OrbitRadius,
                OrbitSpeed = planet.OrbitSpeed,
                OrbitEccentricity = planet.OrbitEccentricity,
                OrbitInclination = planet.OrbitInclination,
                OrbitLongitudeOfAscendingNode = planet.OrbitLongitudeOfAscendingNode,
                OrbitArgumentOfPeriapsis = planet.OrbitArgumentOfPeriapsis,
                OrbitPhaseAtEpoch = planet.OrbitPhaseAtEpoch,
                Size = planet.Size,
                Color = planet.Color,
                Tilt = planet.Tilt,
                HasRing = planet.HasRing,
            }).ToList(),
        };
    }

    private static List<ResourceNodeDefinition> BuildProceduralResourceNodes(
      SolarSystemDefinition template,
      IReadOnlyList<PlanetDefinition> planets,
      int generatedIndex)
    {
        var nodes = new List<ResourceNodeDefinition>();
        if (template.ResourceNodes.Count > 0)
        {
            nodes.AddRange(template.ResourceNodes.Select(node => new ResourceNodeDefinition
            {
                SourceKind = node.SourceKind,
                AnchorReference = node.AnchorReference,
                Angle = node.Angle,
                RadiusOffset = node.RadiusOffset,
                InclinationDegrees = node.InclinationDegrees,
                AnchorPlanetIndex = node.AnchorPlanetIndex,
                AnchorMoonIndex = node.AnchorMoonIndex,
                OreAmount = node.OreAmount,
                ItemId = node.ItemId,
                ShardCount = node.ShardCount,
            }));
        }

        return nodes;
    }

    private static SolarSystemDefinition EnsureStrategicResourceCoverage(SolarSystemDefinition system, int seed)
    {
        for (var index = 0; index < system.ResourceNodes.Count; index += 1)
        {
            system.ResourceNodes[index] = SanitizeResourceNode(system.ResourceNodes[index], system.Planets, seed, index);
        }

        var requiredItems = new[] { "ore", "silicon", "ice", "hydrogen", "helium", "methane" };
        foreach (var itemId in requiredItems)
        {
            if (system.ResourceNodes.Any(node => string.Equals(node.ItemId, itemId, StringComparison.Ordinal)))
            {
                continue;
            }

            system.ResourceNodes.Add(BuildStrategicResourceNode(itemId, system.Planets, seed, system.ResourceNodes.Count));
        }

        return system;
    }

    private static Vector3 BuildGeneratedSystemPosition(IReadOnlyCollection<Vector3> occupiedPositions, int generatedIndex)
    {
        var allPositions = occupiedPositions.ToList();
        for (var attempt = 0; attempt < 64; attempt += 1)
        {
            var candidate = ComputeGeneratedSystemPosition(generatedIndex, attempt);
            if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
            {
                return candidate;
            }
        }

        return ComputeFallbackGeneratedSystemPosition(generatedIndex);
    }

    private static bool ShouldUseKnownSystem(
      WorldGenerationOptions worldGenerationOptions,
      int slotIndex,
      int remainingKnownSystemCount)
    {
        if (!worldGenerationOptions.UseKnownSystems || remainingKnownSystemCount <= 0)
        {
            return false;
        }

        return Hash01(worldGenerationOptions.Seed, 700 + slotIndex) >= KnownSystemSelectionChance;
    }

    private static int SelectKnownSystemIndex(int seed, int slotIndex, int remainingKnownSystemCount)
    {
        var selection = Hash01(seed, 900 + slotIndex);
        return Math.Min((int)(selection * remainingKnownSystemCount), remainingKnownSystemCount - 1);
    }

    private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt)
    {
        const int armCount = 4;
        const float baseInnerRadius = 9f;
        const float radiusStep = 0.54f;
        const float armOffset = MathF.PI * 2f / armCount;

        var armIndex = (generatedIndex + attempt) % armCount;
        var armDepth = generatedIndex / armCount;
        var radius = baseInnerRadius + (armDepth * radiusStep) + Jitter(generatedIndex * 17 + attempt, 0, 0.9f);
        var angle = (armIndex * armOffset) + (radius / 8.2f) + Jitter(generatedIndex, 1 + attempt, 0.16f);
        var x = MathF.Cos(angle) * radius;
        var z = MathF.Sin(angle) * radius * 0.58f;
        var y = ComputeSystemHeight(radius, generatedIndex, attempt);
        return new Vector3(x, y, z);
    }

    private static Vector3 ComputeFallbackGeneratedSystemPosition(int generatedIndex)
    {
        const int ringCount = 5;
        const float fallbackRadius = 42f;
        var angle = (generatedIndex % ringCount) * (MathF.PI * 2f / ringCount) + (generatedIndex / ringCount) * 0.22f;
        var radius = fallbackRadius + (generatedIndex / ringCount) * 1.8f;
        return new Vector3(
          MathF.Cos(angle) * radius,
          ComputeSystemHeight(radius, generatedIndex, 99),
          MathF.Sin(angle) * radius * 0.6f);
    }

    private static string BuildGeneratedSystemId(string label, int ordinal)
    {
        var slug = string.Concat(label
          .ToLowerInvariant()
          .Select(character => char.IsLetterOrDigit(character) ? character : '-'))
          .Trim('-');

        return $"gen-{ordinal}-{slug}";
    }

    private static ResourceNodeDefinition BuildStrategicResourceNode(
      string itemId,
      IReadOnlyList<PlanetDefinition> planets,
      int seed,
      int ordinal)
    {
        var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets);
        return new ResourceNodeDefinition
        {
            SourceKind = "local-space",
            AnchorReference = ResolveStrategicAnchorReference(itemId, planets, ordinal),
            Angle = (MathF.PI * 2f * ((ordinal % 7) / 7f)) + Jitter(seed, 400 + ordinal, 0.35f),
            RadiusOffset = 150000f + Jitter(seed, 460 + ordinal, 42000f),
            InclinationDegrees = Jitter(seed, 520 + ordinal, 10f),
            AnchorPlanetIndex = anchorPlanetIndex,
            OreAmount = itemId switch
            {
                "ore" => 12000f,
                "silicon" => 10000f,
                "ice" => 9000f,
                _ => 8000f,
            },
            ItemId = itemId,
            ShardCount = itemId switch
            {
                "ore" or "silicon" or "ice" => 8,
                _ => 6,
            },
        };
    }

    private static ResourceNodeDefinition SanitizeResourceNode(
      ResourceNodeDefinition node,
      IReadOnlyList<PlanetDefinition> planets,
      int seed,
      int ordinal)
    {
        node.SourceKind = "local-space";
        node.AnchorReference ??= ResolveLegacyAnchorReference(node, planets, seed, ordinal);
        return node;
    }

    private static string ResolveLegacyAnchorReference(
      ResourceNodeDefinition node,
      IReadOnlyList<PlanetDefinition> planets,
      int seed,
      int ordinal)
    {
        if (node.AnchorMoonIndex is int moonIndex && node.AnchorPlanetIndex is int planetIndex && planetIndex >= 0)
        {
            return $"planet-{planetIndex + 1}-moon-{moonIndex + 1}";
        }

        if (node.AnchorPlanetIndex is int anchoredPlanetIndex && anchoredPlanetIndex >= 0)
        {
            return $"planet-{anchoredPlanetIndex + 1}";
        }

        return ResolveStrategicAnchorReference(node.ItemId, planets, ordinal + seed);
    }

    private static string ResolveStrategicAnchorReference(string itemId, IReadOnlyList<PlanetDefinition> planets, int ordinal)
    {
        if (itemId is "hydrogen" or "helium" or "methane")
        {
            var gasGiantIndex = planets
              .Select((planet, index) => (planet, index))
              .FirstOrDefault(entry => entry.planet.PlanetType is "gas-giant" or "ice-giant")
              .index;
            return gasGiantIndex > 0 || (planets.Count > 0 && planets[0].PlanetType is "gas-giant" or "ice-giant")
              ? $"planet-{gasGiantIndex + 1}"
              : "star-1";
        }

        if (itemId == "ice")
        {
            var moonAnchor = planets
              .Select((planet, index) => (planet, index))
              .FirstOrDefault(entry => entry.planet.Moons.Count > 0 && entry.planet.PlanetType is "ice" or "ice-giant" or "oceanic");
            if (moonAnchor.planet is not null && moonAnchor.planet.Moons.Count > 0)
            {
                return $"planet-{moonAnchor.index + 1}-moon-1";
            }
        }

        var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets);
        var lagrange = (ordinal % 3) switch
        {
            0 => "l1",
            1 => "l4",
            _ => "l5",
        };
        return $"planet-{anchorPlanetIndex + 1}-{lagrange}";
    }

    private static int ResolveStrategicResourceAnchorPlanetIndex(string itemId, IReadOnlyList<PlanetDefinition> planets)
    {
        if (planets.Count == 0)
        {
            return 0;
        }

        bool MatchesPlanetType(PlanetDefinition planet) => itemId switch
        {
            "hydrogen" or "helium" or "methane" => planet.PlanetType is "gas-giant" or "ice-giant",
            "ice" => planet.PlanetType is "ice" or "ice-giant" or "oceanic",
            _ => planet.PlanetType is not "gas-giant" and not "ice-giant",
        };

        for (var index = 0; index < planets.Count; index += 1)
        {
            if (MatchesPlanetType(planets[index]))
            {
                return index;
            }
        }

        return ResolveAsteroidAnchorPlanetIndex(planets);
    }

    private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList<PlanetDefinition> planets)
    {
        if (planets.Count == 0)
        {
            return 0;
        }

        var gasGiantIndex = -1;
        for (var index = 0; index < planets.Count; index += 1)
        {
            if (planets[index].PlanetType is "gas-giant" or "ice-giant")
            {
                gasGiantIndex = index;
                break;
            }
        }

        if (gasGiantIndex > 0)
        {
            return gasGiantIndex - 1;
        }

        return Math.Clamp((planets.Count / 2) - 1, 0, planets.Count - 1);
    }

    private static List<PlanetDefinition> BuildGeneratedPlanets(SolarSystemDefinition template, int generatedIndex)
    {
        var planetCount = 2 + (int)MathF.Floor(Hash01(generatedIndex, 2) * 7f);
        var planets = new List<PlanetDefinition>(planetCount);
        var orbitRadius = 0.24f + (Hash01(generatedIndex, 3) * 0.12f);
        var sourcePlanets = template.Planets.Count > 0 ? template.Planets : null;

        for (var index = 0; index < planetCount; index += 1)
        {
            var profile = SelectPlanetProfile(generatedIndex, index);
            var templatePlanet = sourcePlanets is not null && sourcePlanets.Count > 0
              ? sourcePlanets[index % sourcePlanets.Count]
              : null;

            orbitRadius += profile.OrbitGapMin + (Hash01(generatedIndex, 10 + index) * (profile.OrbitGapMax - profile.OrbitGapMin));
            var orbitEccentricity = 0.01f + (Hash01(generatedIndex, 20 + index) * 0.16f);
            var orbitInclination = -9f + (Hash01(generatedIndex, 30 + index) * 18f);
            var moonCount = profile.BaseMoonCount + (int)MathF.Floor(Hash01(generatedIndex, 40 + index) * 3f);
            var planetLabel = $"{BuildPlanetBaseName(generatedIndex, index)}-{index + 1}";

            planets.Add(new PlanetDefinition
            {
                Label = planetLabel,
                PlanetType = profile.Type,
                Shape = profile.Shape,
                Moons = GenerateMoons(planetLabel, profile.BaseSize, moonCount),
                OrbitRadius = orbitRadius,
                OrbitSpeed = 0.11f / MathF.Sqrt(MathF.Max(orbitRadius * orbitRadius * orbitRadius, 0.02f)),
                OrbitEccentricity = orbitEccentricity,
                OrbitInclination = orbitInclination,
                OrbitLongitudeOfAscendingNode = Hash01(generatedIndex, 120 + index) * 360f,
                OrbitArgumentOfPeriapsis = Hash01(generatedIndex, 140 + index) * 360f,
                OrbitPhaseAtEpoch = Hash01(generatedIndex, 160 + index) * 360f,
                Size = profile.BaseSize + (Hash01(generatedIndex, 50 + index) * (profile.BaseSize * 0.35f)),
                Color = templatePlanet?.Color ?? profile.Color,
                Tilt = -0.45f + (Hash01(generatedIndex, 60 + index) * 0.9f),
                HasRing = profile.CanHaveRing && Hash01(generatedIndex, 70 + index) > 0.55f,
            });
        }

        return planets;
    }

    private static StarProfile SelectStarProfile(int generatedIndex)
    {
        var value = Hash01(generatedIndex, 80);
        return value switch
        {
            < 0.32f => StarProfiles[0],
            < 0.54f => StarProfiles[1],
            < 0.68f => StarProfiles[5],
            < 0.8f => StarProfiles[2],
            < 0.9f => StarProfiles[3],
            < 0.97f => StarProfiles[6],
            _ => StarProfiles[4],
        };
    }

    private static PlanetProfile SelectPlanetProfile(int generatedIndex, int planetIndex)
    {
        var value = Hash01(generatedIndex, 90 + planetIndex);
        return value switch
        {
            < 0.14f => PlanetProfiles[7],
            < 0.28f => PlanetProfiles[0],
            < 0.46f => PlanetProfiles[3],
            < 0.62f => PlanetProfiles[1],
            < 0.74f => PlanetProfiles[2],
            < 0.86f => PlanetProfiles[4],
            < 0.94f => PlanetProfiles[6],
            _ => PlanetProfiles[5],
        };
    }

    private static string BuildPlanetBaseName(int generatedIndex, int planetIndex)
    {
        var source = GeneratedSystemNames[generatedIndex % GeneratedSystemNames.Length]
          .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)[0];
        return source[..Math.Min(source.Length, 6)];
    }

    private static float ComputeSystemHeight(float radius, int generatedIndex, int salt)
    {
        var normalized = MathF.Min(1f, MathF.Max(0f, (radius - 8f) / 28f));
        var band = 0.22f + (normalized * 0.76f);
        return (Hash01(generatedIndex, 100 + salt) * 2f - 1f) * band;
    }

    private static float Jitter(int index, int salt, float amplitude) =>
      (Hash01(index, salt) * 2f - 1f) * amplitude;

    // Cheap deterministic pseudo-random helper: same (index, salt) pair always maps to the same 0..1 value.
    // Generation code uses it instead of a mutable RNG so each procedural choice stays stable for a given seed.
    private static float Hash01(int index, int salt)
    {
        uint value = (uint)(index + 1);
        value ^= (uint)(salt + 0x9e3779b9);
        value *= 0x85ebca6b;
        value ^= value >> 13;
        value *= 0xc2b2ae35;
        value ^= value >> 16;
        return (value & 0x00ffffff) / 16777215f;
    }

    private static List<MoonDefinition> GenerateMoons(string planetLabel, float planetSize, int moonCount)
    {
        var seed = planetLabel.Aggregate(0, (acc, c) => acc * 31 + c);
        var moons = new List<MoonDefinition>(moonCount);
        for (var moonIndex = 0; moonIndex < moonCount; moonIndex += 1)
        {
            var spacing = planetSize * 1.4f;
            var radiusVariance = Hash01(seed, 10 + moonIndex) * planetSize * 0.9f;
            var orbitRadius = (planetSize * 1.8f) + (moonIndex * spacing) + radiusVariance;
            var orbitSpeed = 0.9f / MathF.Sqrt(MathF.Max(orbitRadius, 1f)) + (moonIndex * 0.003f);
            var phase = Hash01(seed, 20 + moonIndex) * 360f;
            var inclination = (Hash01(seed, 30 + moonIndex) - 0.5f) * 28f;
            var ascendingNode = Hash01(seed, 40 + moonIndex) * 360f;
            var sizeBase = MathF.Max(2.2f, planetSize * 0.11f);
            var sizeVariance = Hash01(seed, 50 + moonIndex) * MathF.Max(planetSize * 0.16f, 2.5f);
            var size = MathF.Min(sizeBase + sizeVariance, planetSize * 0.42f);

            moons.Add(new MoonDefinition
            {
                Label = $"{planetLabel}-m{moonIndex + 1}",
                Size = size,
                Color = "#c8c4bc",
                OrbitRadius = orbitRadius,
                OrbitSpeed = orbitSpeed,
                OrbitPhaseAtEpoch = phase,
                OrbitInclination = inclination,
                OrbitLongitudeOfAscendingNode = ascendingNode,
            });
        }

        return moons;
    }
}
