using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
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 SubTaskOutcome UpdateSubTask(SimulationWorld world, ShipRuntime ship, ShipPlanStepRuntime step, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        return subTask.Kind switch
        {
            var kind when string.Equals(kind, ShipTaskKinds.Travel, StringComparison.Ordinal) => UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: true),
            var kind when string.Equals(kind, ShipTaskKinds.FollowTarget, StringComparison.Ordinal) => UpdateFollowSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.Dock, StringComparison.Ordinal) => UpdateDockSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.Undock, StringComparison.Ordinal) => UpdateUndockSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.LoadCargo, StringComparison.Ordinal) => UpdateLoadCargoSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.UnloadCargo, StringComparison.Ordinal) => UpdateUnloadCargoSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.TransferCargoToShip, StringComparison.Ordinal) => UpdateTransferCargoToShipSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.MineNode, StringComparison.Ordinal) => UpdateMineSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.SalvageWreck, StringComparison.Ordinal) => UpdateSalvageSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.DeliverConstruction, StringComparison.Ordinal) => UpdateDeliverConstructionSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.BuildConstructionSite, StringComparison.Ordinal) => UpdateBuildConstructionSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.AttackTarget, StringComparison.Ordinal) => UpdateAttackSubTask(world, ship, subTask, deltaSeconds),
            var kind when string.Equals(kind, ShipTaskKinds.HoldPosition, StringComparison.Ordinal) => UpdateHoldSubTask(ship, subTask, deltaSeconds),
            _ => SubTaskOutcome.Failed,
        };
    }

    private SubTaskOutcome UpdateHoldSubTask(ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        ship.State = ShipState.HoldingPosition;
        ship.TargetPosition = subTask.TargetPosition ?? ship.Position;
        ship.Position = ship.Position.MoveToward(ship.TargetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(ship.TargetPosition)));
        return AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.1f, subTask.Amount)) ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateFollowSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f);
        if (targetShip is null)
        {
            subTask.BlockingReason = "follow-target-missing";
            return SubTaskOutcome.Failed;
        }

        var desiredPosition = GetFormationPosition(targetShip.Position, ship.Id, MathF.Max(subTask.Threshold, 16f));
        subTask.TargetSystemId = targetShip.SystemId;
        subTask.TargetPosition = desiredPosition;
        subTask.BlockingReason = null;
        if (ship.SystemId != targetShip.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 12f))
        {
            return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false);
        }

        ship.State = ShipState.HoldingPosition;
        ship.TargetPosition = desiredPosition;
        ship.Position = ship.Position.MoveToward(desiredPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(desiredPosition)));
        return AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.5f, subTask.Amount)) ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateTravelSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds, bool completeOnArrival)
    {
        if (subTask.TargetPosition is null || subTask.TargetSystemId is null)
        {
            subTask.BlockingReason = "travel-target-missing";
            ship.State = ShipState.Blocked;
            return SubTaskOutcome.Failed;
        }

        var targetPosition = ResolveCurrentTargetPosition(world, subTask);
        var targetCelestial = ResolveTravelTargetCelestial(world, subTask, targetPosition);
        ship.TargetPosition = targetPosition;

        if (ship.SystemId != subTask.TargetSystemId)
        {
            if (!CanFtl(ship.Definition))
            {
                subTask.BlockingReason = "ftl-unavailable";
                ship.State = ShipState.Blocked;
                return SubTaskOutcome.Failed;
            }

            var destinationEntryCelestial = ResolveSystemEntryCelestial(world, subTask.TargetSystemId);
            var destinationEntryPosition = destinationEntryCelestial?.Position ?? targetPosition;
            return UpdateFtlTransit(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, destinationEntryPosition, destinationEntryCelestial, completeOnArrival, targetPosition);
        }

        var currentCelestial = ResolveCurrentCelestial(world, ship);
        if (targetCelestial is not null
            && currentCelestial is not null
            && !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal))
        {
            if (!CanWarp(ship.Definition))
            {
                return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, completeOnArrival);
            }

            return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival);
        }

        if (targetCelestial is not null
            && ship.Position.DistanceTo(targetPosition) > WarpEngageDistanceKilometers
            && CanWarp(ship.Definition))
        {
            return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival);
        }

        return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, completeOnArrival);
    }

    private SubTaskOutcome UpdateAttackSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        var hostileShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f);
        var hostileStation = hostileShip is null
          ? world.Stations.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId)
          : null;
        if ((hostileShip is not null && hostileShip.FactionId == ship.FactionId)
            || (hostileStation is not null && hostileStation.FactionId == ship.FactionId))
        {
            subTask.BlockingReason = "friendly-target";
            return SubTaskOutcome.Failed;
        }

        if (hostileShip is null && hostileStation is null)
        {
            return SubTaskOutcome.Completed;
        }

        var targetSystemId = hostileShip?.SystemId ?? hostileStation!.SystemId;
        var targetPosition = hostileShip?.Position ?? hostileStation!.Position;
        var attackRange = hostileShip is null ? hostileStation!.Radius + 18f : 26f;
        subTask.TargetSystemId = targetSystemId;
        subTask.TargetPosition = targetPosition;
        subTask.Threshold = attackRange;

        if (ship.SystemId != targetSystemId || ship.Position.DistanceTo(targetPosition) > attackRange)
        {
            return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false);
        }

        ship.State = ShipState.EngagingTarget;
        ship.TargetPosition = targetPosition;
        ship.Position = ship.Position.MoveToward(targetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, 8f));
        var damage = GetShipDamagePerSecond(ship) * deltaSeconds * GetSkillFactor(ship.Skills.Combat);
        subTask.Progress = 1f;

        if (hostileShip is not null)
        {
            hostileShip.Health = MathF.Max(0f, hostileShip.Health - damage);
            return hostileShip.Health <= 0f ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
        }

        hostileStation!.Health = MathF.Max(0f, hostileStation.Health - (damage * 0.6f));
        return hostileStation.Health <= 0f ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateMineSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        var node = ResolveNode(world, subTask.TargetEntityId ?? subTask.TargetNodeId);
        if (node is null || !CanExtractNode(ship, node, world))
        {
            subTask.BlockingReason = "node-missing";
            ship.State = ShipState.Blocked;
            return SubTaskOutcome.Failed;
        }

        var targetPosition = subTask.TargetPosition ?? GetResourceHoldPosition(node.Position, ship.Id, 20f);
        ship.TargetPosition = targetPosition;
        if (ship.Position.DistanceTo(targetPosition) > MathF.Max(subTask.Threshold, 8f))
        {
            ship.State = ShipState.MiningApproach;
            ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
            return SubTaskOutcome.Active;
        }

        var cargoAmount = GetShipCargoAmount(ship);
        if (cargoAmount >= ship.Definition.GetTotalCargoCapacity() - 0.01f)
        {
            return SubTaskOutcome.Completed;
        }

        ship.State = ShipState.Mining;
        if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.MiningCycleSeconds))
        {
            return SubTaskOutcome.Active;
        }

        var remainingCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - cargoAmount);
        var mined = MathF.Min(balance.MiningRate * GetSkillFactor(ship.Skills.Mining), remainingCapacity);
        mined = MathF.Min(mined, node.OreRemaining);
        if (mined <= 0.01f)
        {
            return SubTaskOutcome.Completed;
        }

        AddInventory(ship.Inventory, node.ItemId, mined);
        node.OreRemaining = MathF.Max(0f, node.OreRemaining - mined);
        if (GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f || node.OreRemaining <= 0.01f)
        {
            return SubTaskOutcome.Completed;
        }

        subTask.ElapsedSeconds = 0f;
        return SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateDockSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        var station = ResolveStation(world, subTask.TargetEntityId);
        if (station is null)
        {
            subTask.BlockingReason = "dock-target-missing";
            ship.State = ShipState.Blocked;
            return SubTaskOutcome.Failed;
        }

        var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id);
        if (padIndex is null)
        {
            ship.State = ShipState.AwaitingDock;
            ship.TargetPosition = GetDockingHoldPosition(station, ship.Id);
            if (ship.Position.DistanceTo(ship.TargetPosition) > 4f)
            {
                ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
            }

            subTask.Status = WorkStatus.Blocked;
            subTask.BlockingReason = "waiting-for-pad";
            return SubTaskOutcome.Active;
        }

        subTask.Status = WorkStatus.Active;
        subTask.BlockingReason = null;
        ship.AssignedDockingPadIndex = padIndex;
        var padPosition = GetDockingPadPosition(station, padIndex.Value);
        ship.TargetPosition = padPosition;
        if (ship.Position.DistanceTo(padPosition) > 4f)
        {
            ship.State = ShipState.DockingApproach;
            ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
            return SubTaskOutcome.Active;
        }

        ship.State = ShipState.Docking;
        if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.DockingDuration))
        {
            return SubTaskOutcome.Active;
        }

        ship.State = ShipState.Docked;
        ship.DockedStationId = station.Id;
        station.DockedShipIds.Add(ship.Id);
        ship.KnownStationIds.Add(station.Id);
        ship.Position = padPosition;
        ship.TargetPosition = padPosition;
        return SubTaskOutcome.Completed;
    }

    private SubTaskOutcome UpdateUndockSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        if (ship.DockedStationId is null)
        {
            return SubTaskOutcome.Completed;
        }

        var station = ResolveStation(world, ship.DockedStationId);
        if (station is null)
        {
            ship.DockedStationId = null;
            ship.AssignedDockingPadIndex = null;
            return SubTaskOutcome.Completed;
        }

        var undockTarget = GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, balance.UndockDistance);
        ship.TargetPosition = undockTarget;
        ship.State = ShipState.Undocking;
        if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.UndockingDuration))
        {
            ship.Position = GetShipDockedPosition(ship, station);
            return SubTaskOutcome.Active;
        }

        ship.Position = ship.Position.MoveToward(undockTarget, balance.UndockDistance);
        if (ship.Position.DistanceTo(undockTarget) > MathF.Max(subTask.Threshold, 4f))
        {
            return SubTaskOutcome.Active;
        }

        station.DockedShipIds.Remove(ship.Id);
        ReleaseDockingPad(station, ship.Id);
        ship.DockedStationId = null;
        ship.AssignedDockingPadIndex = null;
        return SubTaskOutcome.Completed;
    }

    private SubTaskOutcome UpdateLoadCargoSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        if (ship.DockedStationId is null)
        {
            subTask.BlockingReason = "not-docked";
            return SubTaskOutcome.Failed;
        }

        var station = ResolveStation(world, ship.DockedStationId);
        if (station is null)
        {
            subTask.BlockingReason = "station-missing";
            return SubTaskOutcome.Failed;
        }

        ship.TargetPosition = GetShipDockedPosition(ship, station);
        ship.Position = ship.TargetPosition;
        ship.State = ShipState.Loading;
        var itemId = subTask.ItemId;
        if (itemId is null)
        {
            return SubTaskOutcome.Completed;
        }

        var desiredAmount = subTask.Amount > 0f ? subTask.Amount : ship.Definition.GetTotalCargoCapacity();
        var availableCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - GetShipCargoAmount(ship));
        var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Trade);
        var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(availableCapacity, GetInventoryAmount(station.Inventory, itemId)));
        if (moved > 0.01f)
        {
            RemoveInventory(station.Inventory, itemId, moved);
            AddInventory(ship.Inventory, itemId, moved);
        }

        var loadedAmount = GetInventoryAmount(ship.Inventory, itemId);
        subTask.Progress = desiredAmount <= 0.01f ? 1f : Math.Clamp(loadedAmount / desiredAmount, 0f, 1f);
        return availableCapacity <= 0.01f || GetInventoryAmount(station.Inventory, itemId) <= 0.01f || loadedAmount >= desiredAmount - 0.01f
          ? SubTaskOutcome.Completed
          : SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateUnloadCargoSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        if (ship.DockedStationId is null)
        {
            subTask.BlockingReason = "not-docked";
            return SubTaskOutcome.Failed;
        }

        var station = ResolveStation(world, ship.DockedStationId);
        if (station is null)
        {
            subTask.BlockingReason = "station-missing";
            return SubTaskOutcome.Failed;
        }

        ship.TargetPosition = GetShipDockedPosition(ship, station);
        ship.Position = ship.TargetPosition;
        ship.State = ShipState.Transferring;
        var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Mining));

        if (subTask.ItemId is not null)
        {
            var moved = MathF.Min(transferRate * deltaSeconds, GetInventoryAmount(ship.Inventory, subTask.ItemId));
            var accepted = TryAddStationInventory(world, station, subTask.ItemId, moved);
            RemoveInventory(ship.Inventory, subTask.ItemId, accepted);
            subTask.Progress = subTask.Amount <= 0.01f
              ? 1f
              : Math.Clamp(1f - (GetInventoryAmount(ship.Inventory, subTask.ItemId) / subTask.Amount), 0f, 1f);
            return GetInventoryAmount(ship.Inventory, subTask.ItemId) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
        }

        foreach (var (itemId, amount) in ship.Inventory.ToList().OrderBy(entry => entry.Key, StringComparer.Ordinal))
        {
            var moved = MathF.Min(amount, transferRate * deltaSeconds);
            var accepted = TryAddStationInventory(world, station, itemId, moved);
            RemoveInventory(ship.Inventory, itemId, accepted);
            if (accepted > 0.01f)
            {
                return GetShipCargoAmount(ship) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
            }
        }

        return GetShipCargoAmount(ship) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateTransferCargoToShipSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f);
        if (targetShip is null)
        {
            subTask.BlockingReason = "target-ship-missing";
            return SubTaskOutcome.Failed;
        }

        var desiredPosition = GetFormationPosition(targetShip.Position, ship.Id, MathF.Max(subTask.Threshold, 12f));
        subTask.TargetSystemId = targetShip.SystemId;
        subTask.TargetPosition = desiredPosition;
        if (ship.SystemId != targetShip.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 12f))
        {
            return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false);
        }

        ship.State = ShipState.Transferring;
        ship.TargetPosition = desiredPosition;
        ship.Position = ship.Position.MoveToward(desiredPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(desiredPosition)));
        if (subTask.ItemId is null)
        {
            return SubTaskOutcome.Completed;
        }

        var targetCapacity = MathF.Max(0f, targetShip.Definition.GetTotalCargoCapacity() - GetShipCargoAmount(targetShip));
        if (targetCapacity <= 0.01f)
        {
            subTask.BlockingReason = "target-cargo-full";
            return SubTaskOutcome.Failed;
        }

        var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Navigation));
        var desiredAmount = subTask.Amount > 0f ? subTask.Amount : GetInventoryAmount(ship.Inventory, subTask.ItemId);
        var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(targetCapacity, GetInventoryAmount(ship.Inventory, subTask.ItemId)));
        if (moved > 0.01f)
        {
            RemoveInventory(ship.Inventory, subTask.ItemId, moved);
            AddInventory(targetShip.Inventory, subTask.ItemId, moved);
        }

        var remaining = GetInventoryAmount(ship.Inventory, subTask.ItemId);
        subTask.Progress = desiredAmount <= 0.01f ? 1f : Math.Clamp(1f - (remaining / desiredAmount), 0f, 1f);
        return remaining <= 0.01f || GetShipCargoAmount(targetShip) >= targetShip.Definition.GetTotalCargoCapacity() - 0.01f
          ? SubTaskOutcome.Completed
          : SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateSalvageSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.RemainingAmount > 0.01f);
        if (wreck is null)
        {
            return SubTaskOutcome.Completed;
        }

        var desiredPosition = subTask.TargetPosition ?? GetFormationPosition(wreck.Position, ship.Id, 8f);
        ship.TargetPosition = desiredPosition;
        if (ship.SystemId != wreck.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 8f))
        {
            subTask.TargetSystemId = wreck.SystemId;
            subTask.TargetPosition = desiredPosition;
            return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false);
        }

        ship.State = ShipState.Transferring;
        var remainingCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - GetShipCargoAmount(ship));
        if (remainingCapacity <= 0.01f)
        {
            return SubTaskOutcome.Completed;
        }

        if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.4f, balance.MiningCycleSeconds * 0.8f)))
        {
            return SubTaskOutcome.Active;
        }

        var salvageRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Mining, ship.Skills.Trade));
        var recovered = MathF.Min(salvageRate, MathF.Min(remainingCapacity, wreck.RemainingAmount));
        if (recovered > 0.01f)
        {
            AddInventory(ship.Inventory, wreck.ItemId, recovered);
            wreck.RemainingAmount = MathF.Max(0f, wreck.RemainingAmount - recovered);
        }

        if (wreck.RemainingAmount <= 0.01f)
        {
            world.Wrecks.RemoveAll(candidate => candidate.Id == wreck.Id);
        }

        subTask.ElapsedSeconds = 0f;
        return wreck.RemainingAmount <= 0.01f || GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f
          ? SubTaskOutcome.Completed
          : SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateDeliverConstructionSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
        var station = site is null ? null : ResolveSupportStation(world, ship, site);
        if (site is null || station is null || site.State is ConstructionSiteStateKinds.Completed or ConstructionSiteStateKinds.Destroyed)
        {
            subTask.BlockingReason = "construction-target-missing";
            return SubTaskOutcome.Failed;
        }

        var supportPosition = ResolveSupportPosition(ship, station, site, world);
        if (!IsShipWithinSupportRange(ship, supportPosition, MathF.Max(12f, subTask.Threshold)))
        {
            ship.State = ShipState.LocalFlight;
            ship.TargetPosition = supportPosition;
            ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
            return SubTaskOutcome.Active;
        }

        ship.TargetPosition = supportPosition;
        ship.Position = supportPosition;
        ship.State = ShipState.DeliveringConstruction;
        var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Construction);
        foreach (var required in site.RequiredItems.OrderBy(entry => entry.Key, StringComparer.Ordinal))
        {
            var delivered = GetInventoryAmount(site.DeliveredItems, required.Key);
            var remaining = MathF.Max(0f, required.Value - delivered);
            if (remaining <= 0.01f)
            {
                continue;
            }

            var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key));
            var moved = MathF.Min(remaining, MathF.Min(available, transferRate * deltaSeconds));
            if (moved <= 0.01f)
            {
                continue;
            }

            RemoveInventory(station.Inventory, required.Key, moved);
            AddInventory(site.Inventory, required.Key, moved);
            AddInventory(site.DeliveredItems, required.Key, moved);
            break;
        }

        subTask.Progress = site.RequiredItems.Count == 0
          ? 1f
          : site.RequiredItems.Sum(required =>
              required.Value <= 0.01f
                ? 1f
                : Math.Clamp(GetInventoryAmount(site.DeliveredItems, required.Key) / required.Value, 0f, 1f)) / site.RequiredItems.Count;
        return IsConstructionSiteReady(world, site) ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateBuildConstructionSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
    {
        var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
        var station = site is null ? null : ResolveSupportStation(world, ship, site);
        if (site is null || station is null || site.BlueprintId is null || site.State is ConstructionSiteStateKinds.Completed or ConstructionSiteStateKinds.Destroyed)
        {
            subTask.BlockingReason = "construction-site-missing";
            return SubTaskOutcome.Failed;
        }

        var supportPosition = ResolveSupportPosition(ship, station, site, world);
        if (!IsShipWithinSupportRange(ship, supportPosition, MathF.Max(12f, subTask.Threshold)))
        {
            ship.State = ShipState.LocalFlight;
            ship.TargetPosition = supportPosition;
            ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
            return SubTaskOutcome.Active;
        }

        if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe))
        {
            ship.State = ShipState.WaitingMaterials;
            subTask.Status = WorkStatus.Blocked;
            subTask.BlockingReason = "waiting-materials";
            return SubTaskOutcome.Active;
        }

        subTask.Status = WorkStatus.Active;
        subTask.BlockingReason = null;
        ship.TargetPosition = supportPosition;
        ship.Position = supportPosition;
        ship.State = ShipState.Constructing;
        site.AssignedConstructorShipIds.Add(ship.Id);
        site.Progress += deltaSeconds * GetSkillFactor(ship.Skills.Construction);
        subTask.Progress = recipe.Duration <= 0.01f ? 1f : Math.Clamp(site.Progress / recipe.Duration, 0f, 1f);
        if (site.Progress < recipe.Duration)
        {
            return SubTaskOutcome.Active;
        }

        if (site.StationId is null)
        {
            CompleteStationFoundation(world, station, site);
        }
        else
        {
            AddStationModule(world, station, site.BlueprintId);
            PrepareNextConstructionSiteStep(world, station, site);
        }

        site.State = ConstructionSiteStateKinds.Completed;
        return SubTaskOutcome.Completed;
    }

    private static bool AdvanceTimedSubTask(ShipSubTaskRuntime subTask, float deltaSeconds, float requiredSeconds)
    {
        subTask.TotalSeconds = requiredSeconds;
        subTask.ElapsedSeconds += deltaSeconds;
        subTask.Progress = requiredSeconds <= 0.01f ? 1f : Math.Clamp(subTask.ElapsedSeconds / requiredSeconds, 0f, 1f);
        if (subTask.ElapsedSeconds < requiredSeconds)
        {
            return false;
        }

        subTask.ElapsedSeconds = 0f;
        return true;
    }

    private SubTaskOutcome UpdateLocalTravel(
      SimulationWorld world,
      ShipRuntime ship,
      ShipSubTaskRuntime subTask,
      float deltaSeconds,
      string targetSystemId,
      Vector3 targetPosition,
      CelestialRuntime? targetCelestial,
      bool completeOnArrival)
    {
        var distance = ship.Position.DistanceTo(targetPosition);
        ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
        ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
        ship.SpatialState.Transit = null;
        ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
        subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f);

        if (distance <= MathF.Max(subTask.Threshold, balance.ArrivalThreshold))
        {
            ship.Position = targetPosition;
            ship.TargetPosition = targetPosition;
            ship.SystemId = targetSystemId;
            ship.SpatialState.CurrentSystemId = targetSystemId;
            ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
            ship.State = ShipState.Arriving;
            return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
        }

        ship.State = ShipState.LocalFlight;
        ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
        return SubTaskOutcome.Active;
    }

    private SubTaskOutcome UpdateWarpTransit(
      SimulationWorld world,
      ShipRuntime ship,
      ShipSubTaskRuntime subTask,
      float deltaSeconds,
      Vector3 targetPosition,
      CelestialRuntime targetCelestial,
      bool completeOnArrival)
    {
        var transit = ship.SpatialState.Transit;
        if (transit is null || transit.Regime != MovementRegimeKind.Warp || transit.DestinationNodeId != targetCelestial.Id)
        {
            transit = new ShipTransitRuntime
            {
                Regime = MovementRegimeKind.Warp,
                OriginNodeId = ship.SpatialState.CurrentCelestialId,
                DestinationNodeId = targetCelestial.Id,
                StartedAtUtc = world.GeneratedAtUtc,
            };
            ship.SpatialState.Transit = transit;
            subTask.ElapsedSeconds = 0f;
        }

        ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace;
        ship.SpatialState.MovementRegime = MovementRegimeKind.Warp;
        ship.SpatialState.CurrentCelestialId = null;
        ship.SpatialState.DestinationNodeId = targetCelestial.Id;

        var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
        if (ship.State != ShipState.Warping)
        {
            ship.State = ShipState.SpoolingWarp;
            if (!AdvanceTimedSubTask(subTask, deltaSeconds, spoolDuration))
            {
                return SubTaskOutcome.Active;
            }

            ship.State = ShipState.Warping;
        }

        var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
          ? ship.Position.DistanceTo(targetPosition)
          : (world.Celestials.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
        ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
        transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
        subTask.Progress = transit.Progress;
        if (ship.Position.DistanceTo(targetPosition) > 18f)
        {
            return SubTaskOutcome.Active;
        }

        return CompleteTransitArrival(ship, subTask.TargetSystemId ?? ship.SystemId, targetPosition, targetCelestial, completeOnArrival);
    }

    private SubTaskOutcome UpdateFtlTransit(
      SimulationWorld world,
      ShipRuntime ship,
      ShipSubTaskRuntime subTask,
      float deltaSeconds,
      string targetSystemId,
      Vector3 entryPosition,
      CelestialRuntime? targetCelestial,
      bool completeOnArrival,
      Vector3 finalTargetPosition)
    {
        var destinationNodeId = targetCelestial?.Id;
        var transit = ship.SpatialState.Transit;
        if (transit is null || transit.Regime != MovementRegimeKind.FtlTransit || transit.DestinationNodeId != destinationNodeId)
        {
            transit = new ShipTransitRuntime
            {
                Regime = MovementRegimeKind.FtlTransit,
                OriginNodeId = ship.SpatialState.CurrentCelestialId,
                DestinationNodeId = destinationNodeId,
                StartedAtUtc = world.GeneratedAtUtc,
            };
            ship.SpatialState.Transit = transit;
            subTask.ElapsedSeconds = 0f;
        }

        ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace;
        ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit;
        ship.SpatialState.CurrentCelestialId = null;
        ship.SpatialState.DestinationNodeId = destinationNodeId;

        if (ship.State != ShipState.Ftl)
        {
            ship.State = ShipState.SpoolingFtl;
            if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(ship.Definition.SpoolTime, 0.1f)))
            {
                return SubTaskOutcome.Active;
            }

            ship.State = ShipState.Ftl;
        }

        var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
        var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
        var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
        transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * GetSkillFactor(ship.Skills.Navigation)) * deltaSeconds / totalDistance));
        subTask.Progress = transit.Progress;
        if (transit.Progress < 0.999f)
        {
            return SubTaskOutcome.Active;
        }

        ship.Position = entryPosition;
        ship.TargetPosition = finalTargetPosition;
        ship.SystemId = targetSystemId;
        ship.SpatialState.CurrentSystemId = targetSystemId;
        ship.SpatialState.Transit = null;
        ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
        ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
        ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
        ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
        ship.State = ShipState.Arriving;

        // Cross-system travel is only complete once the ship finishes the
        // destination-system local leg to the actual target.
        return SubTaskOutcome.Active;
    }

    private static SubTaskOutcome CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial, bool completeOnArrival)
    {
        ship.Position = targetPosition;
        ship.TargetPosition = targetPosition;
        ship.SystemId = targetSystemId;
        ship.SpatialState.CurrentSystemId = targetSystemId;
        ship.SpatialState.Transit = null;
        ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
        ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
        ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
        ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
        ship.State = ShipState.Arriving;
        return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
    }
}
