namespace SpaceGame.Api.Simulation.Core;

internal sealed class SimulationEngine
{
    private readonly IBalanceService _balance;
    private readonly IPlayerStateStore _playerStateStore;
    private readonly OrbitalSimulationOptions _orbitalSimulation;
    private readonly OrbitalStateUpdater _orbitalStateUpdater;
    private readonly InfrastructureSimulationService _infrastructureSimulation;
    private readonly GeopoliticalSimulationService _geopolitics;
    private readonly CommanderPlanningService _commanderPlanning;
    private readonly PlayerFactionService _playerFaction;
    private readonly StationSimulationService _stationSimulation;
    private readonly StationLifecycleService _stationLifecycle;
    private readonly ShipAiService _shipAi;
    private readonly SimulationProjectionService _projection;

    internal SimulationEngine(OrbitalSimulationOptions orbitalSimulation, IBalanceService balance, IPlayerStateStore playerStateStore)
    {
        _balance = balance;
        _playerStateStore = playerStateStore;
        _orbitalSimulation = orbitalSimulation;
        _orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation);
        _infrastructureSimulation = new InfrastructureSimulationService();
        _geopolitics = new GeopoliticalSimulationService();
        _commanderPlanning = new CommanderPlanningService();
        _playerFaction = new PlayerFactionService();
        _stationSimulation = new StationSimulationService();
        _stationLifecycle = new StationLifecycleService(_stationSimulation);
        _shipAi = new ShipAiService(balance);
        _projection = new SimulationProjectionService(_orbitalSimulation);
    }

    public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
    {
        var nowUtc = DateTimeOffset.UtcNow;
        var events = new List<SimulationEventRecord>();
        var simulationDeltaSeconds = deltaSeconds * MathF.Max(_balance.SimulationSpeedMultiplier, 0.01f);
        world.GeneratedAtUtc = nowUtc;

        world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;

        _orbitalStateUpdater.Update(world);
        _infrastructureSimulation.UpdateClaims(world, events);
        _infrastructureSimulation.UpdateConstructionSites(world, events);
        _geopolitics.Update(world, simulationDeltaSeconds, events);
        _commanderPlanning.UpdateCommanders(world, _playerStateStore, simulationDeltaSeconds, events);
        _playerFaction.Update(world, _playerStateStore, simulationDeltaSeconds, events);
        _stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events);

        foreach (var ship in world.Ships.ToList())
        {
            if (ship.Health <= 0f)
            {
                continue;
            }

            var previousPosition = ship.Position;
            _shipAi.UpdateShip(world, ship, simulationDeltaSeconds, events);
            ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds);
        }

        _orbitalStateUpdater.SyncSpatialState(world);
        CleanupDestroyedEntities(world, events);
        return _projection.BuildDelta(world, sequence, events);
    }

    public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence) =>
      _projection.BuildSnapshot(world, sequence);

    public void PrimeDeltaBaseline(SimulationWorld world) =>
      _projection.PrimeDeltaBaseline(world);

    internal static float GetShipCargoAmount(ShipRuntime ship) =>
      SimulationRuntimeSupport.GetShipCargoAmount(ship);

    private static void CleanupDestroyedEntities(SimulationWorld world, ICollection<SimulationEventRecord> events)
    {
        foreach (var ship in world.Ships.Where(candidate => candidate.Health <= 0f).ToList())
        {
            CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.GetTotalCargoCapacity() + (ship.Definition.Hull * 0.08f));
            world.Ships.Remove(ship);
            if (ship.DockedStationId is not null && world.Stations.FirstOrDefault(station => station.Id == ship.DockedStationId) is { } dockedStation)
            {
                dockedStation.DockedShipIds.Remove(ship.Id);
                dockedStation.DockingPadAssignments.Remove(ship.AssignedDockingPadIndex ?? -1);
            }

            if (world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId) is { } faction)
            {
                faction.ShipsLost += 1;
            }

            if (ship.CommanderId is not null && world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId) is { } commander)
            {
                commander.IsAlive = false;
            }

            events.Add(new SimulationEventRecord("ship", ship.Id, "destroyed", $"{ship.Definition.Name} was destroyed.", DateTimeOffset.UtcNow));
        }

        foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList())
        {
            CreateWreck(world, "station", station.Id, station.SystemId, station.Position, station.MaxHealth * 0.12f);
            world.Stations.Remove(station);

            if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial)
            {
                celestial.OccupyingStructureId = null;
            }

            foreach (var claim in world.Claims.Where(candidate => candidate.CelestialId == station.CelestialId))
            {
                claim.Health = 0f;
                claim.State = ClaimStateKinds.Destroyed;
            }

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

            events.Add(new SimulationEventRecord("station", station.Id, "destroyed", $"{station.Label} was destroyed.", DateTimeOffset.UtcNow));
        }
    }

    private static void CreateWreck(SimulationWorld world, string sourceKind, string sourceEntityId, string systemId, Vector3 position, float amount)
    {
        var itemId = world.ItemDefinitions.ContainsKey("scrapmetal")
          ? "scrapmetal"
          : world.ItemDefinitions.ContainsKey("rawscrap")
            ? "rawscrap"
            : world.ItemDefinitions.Keys.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
        if (itemId is null || amount <= 0.01f)
        {
            return;
        }

        world.Wrecks.Add(new WreckRuntime
        {
            Id = $"wreck-{sourceKind}-{sourceEntityId}",
            SourceKind = sourceKind,
            SourceEntityId = sourceEntityId,
            SystemId = systemId,
            Position = position,
            ItemId = itemId,
            RemainingAmount = amount,
            MaxAmount = amount,
        });
    }
}
