using System;
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;

namespace CounterDrone.Core.Algorithms
{
    /// <summary>防御规划器 — 五步流水线，全部调用与引擎共享的物理工具类</summary>
    public class DefensePlanner : IDefensePlanner
    {
        private readonly List<AmmunitionSpec> _ammoCatalog;
        private readonly IDamageModel _damageModel;
        private readonly PlannerConfig _config;

        public DefensePlanner(List<AmmunitionSpec> ammoCatalog, PlannerConfig config,
            IDamageModel? damageModel = null)
        {
            _ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog));
            _config = config ?? throw new ArgumentNullException(nameof(config));
            _damageModel = damageModel ?? new DamageModelRouter();
            if (_ammoCatalog.Count == 0)
                throw new ArgumentException("弹药规格目录不能为空");
        }

        // ═══════════════════════════════════════════════
        // 五步流水线
        // ═══════════════════════════════════════════════

        public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats,
            CombatScene environment, List<DetectionSource> detectionSources)
        {
            var result = new PlannerResult();

            if (threats.Count == 0)
            {
                result.Best.Summary = "无威胁目标";
                return result;
            }
            if (fireUnits.Count == 0)
            {
                result.Best.Summary = "无可用火力单元";
                result.Best.ThreatsUnengaged = threats.Count;
                return result;
            }

            // 探测信息：每个威胁的最早发现弧长 + 精度（基于统一信息网络）
            // 无探测设备时 detectArc=0（上帝视角，从航路起点算）、精度用配置默认值
            float visibility = (float)environment.Visibility;
            foreach (var t in threats)
            {
                var (detectArc, accuracy) = DetectionCalculator.EarliestDetection(
                    t.Waypoints, detectionSources, visibility);
                t.DetectArc = detectArc;
                t.DetectAccuracy = accuracy == float.MaxValue
                    ? _config.DefaultDetectionAccuracy
                    : accuracy;
            }

            // Step 1: 威胁排序
            foreach (var t in threats)
            {
                t.ArrivalTime = t.GetArrivalTime();
                t.ThreatIndex = CalcThreatIndex(_config, t.Target);
            }
            var sorted = threats.OrderByDescending(t => t.Priority).ToList();

            // Step 2-4: 贪心分配求解
            // 预先检查：所有需要的弹药类型都在目录中
            var neededTypes = sorted
                .Select(t => MatchAmmo(_config, (PowerType)t.Target.PowerType))
                .Distinct()
                .ToList();
            foreach (var t in neededTypes)
            {
                if (!_ammoCatalog.Any(a => a.AerosolType == (int)t))
                    throw new InvalidOperationException($"弹药规格目录中缺少类型: {t}");
            }

            result.Best = Solve(sorted, fireUnits, environment);

            // Step 5: 临界方案
            result.Critical = DeriveCritical(result.Best);

            return result;
        }

        // ═══════════════════════════════════════════════
        // Step 1: 威胁指数
        // ═══════════════════════════════════════════════

        private static float CalcThreatIndex(PlannerConfig config, TargetConfig target)
        {
            float typeCoef = config.TypeCoefficient.GetValueOrDefault((TargetType)target.TargetType, 1f);
            float speedCoef = (float)target.TypicalSpeed / 60f;
            return typeCoef * speedCoef;
        }

        // ═══════════════════════════════════════════════
        // Step 2: 弹药匹配
        // ═══════════════════════════════════════════════

        private static AerosolType MatchAmmo(PlannerConfig config, PowerType power)
        {
            return config.AmmoMatch.GetValueOrDefault(power, AerosolType.InertGas);
        }

        // ═══════════════════════════════════════════════
        // Step 3-4: 候选生成 → 贪心分配
        // ═══════════════════════════════════════════════

        private DefensePlan Solve(List<DroneWave> sortedThreats,
            List<FireUnit> fireUnits, CombatScene env)
        {
            var plan = new DefensePlan();
            var remainingMunitions = new Dictionary<string, int>();
            foreach (var u in fireUnits)
                remainingMunitions[u.Id] = u.TotalMunitions;

            foreach (var threat in sortedThreats)
            {
                var available = fireUnits
                    .Where(u => remainingMunitions.GetValueOrDefault(u.Id, 0) > 0)
                    .ToList();

                var candidates = GenerateCandidates(threat, available, env);
                if (candidates.Count == 0)
                {
                    plan.ThreatsUnengaged++;
                    continue;
                }

                // 计算总弹药需求
                var neededAmmo = MatchAmmo(_config, (PowerType)threat.Target.PowerType);
                var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);

                // 单机需求
                var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(ammo, env);
                int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false, turbulentRadius);

                // 横向编队：每种 Width = Quantity 个独立车道
                int yLanes = 1;
                float formationWidth = 0f;
                if (threat.Target.Quantity > 1 && threat.Route != null && (FormationMode)threat.Route.FormationMode == FormationMode.Formation)
                {
                    yLanes = threat.Target.Quantity;
                    formationWidth = (threat.Target.Quantity - 1) * (float)threat.Route.LateralSpacing;
                }
                int totalRoundsNeeded = singleNeeded * yLanes;

                // 逐单元分配：每个单元锁定一个 Y 车道
                // 云团间距 = 2R × (1 - 重叠比例)，重叠由配置驱动，保证有效区互相覆盖
                float spacing = 2f * turbulentRadius * (1f - _config.CloudOverlapRatio);
                int[] laneNeeded = new int[yLanes];
                float[] laneBaseTime = new float[yLanes];
                bool[] laneBaseSet = new bool[yLanes];
                for (int l = 0; l < yLanes; l++) laneNeeded[l] = singleNeeded;
                int currentLane = 0;
                int roundsCollected = 0;
                var assignedUnits = new List<(FireUnit unit, int rounds, List<FireEvent> events)>();

                foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
                    .ThenByDescending(c => c.KillProbability))
                {
                    if (roundsCollected >= totalRoundsNeeded) break;
                    int remaining = remainingMunitions.GetValueOrDefault(c.Unit.Id, 0);
                    if (remaining <= 0) continue;

                    while (currentLane < yLanes && laneNeeded[currentLane] <= 0) currentLane++;
                    if (currentLane >= yLanes) break;

                    int toTake = Math.Min(Math.Min(c.Unit.TotalChannels, remaining), laneNeeded[currentLane]);
                    if (toTake <= 0) continue;

                    int yLane = currentLane;
                    // 车道第一个单元设基准时间，后续单元以此为准保证间距均匀
                    if (!laneBaseSet[yLane])
                    {
                        var (refEvt, refRej) = TryGenerateFireEvents(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth);
                        if (refEvt.Count == 0) { plan.RejectReason ??= refRej; continue; }
                        laneBaseTime[yLane] = refEvt[0].FireTime;
                        laneBaseSet[yLane] = true;
                    }
                    float stagger = Kinematics.CloudCoverInterval(turbulentRadius * 2f, GetDroneSpeedKph(threat), c.Unit.ChannelInterval);

                    var fevents = new List<FireEvent>();
                    int unitIdx = fireUnits.IndexOf(c.Unit);
                    int baseRoundInLane = singleNeeded - laneNeeded[currentLane];
                    for (int ch = 0; ch < toTake; ch++)
                    {
                        // offset 基于车道内发序（不是全局 eventIdx），使每车道独立分布在同一航路段，
                        // 多车道重叠覆盖而非沿航路连成长链
                        int roundInLane = baseRoundInLane + ch;
                        float offset = (roundInLane - (singleNeeded - 1) / 2f) * spacing;
                        var (fe, feRej) = TryGenerateFireEvents(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth);
                        if (fe.Count == 0) { plan.RejectReason ??= feRej; continue; }
                        foreach (var e in fe)
                        {
                            e.FireTime = laneBaseTime[yLane] + roundInLane * stagger;
                            // TargetX/Z 保留 GenerateFireEventsAt 算出的风偏补偿后抛撒点 cloudGenX/Z
                            e.PlatformIndex = unitIdx * c.Unit.TotalChannels + ch;
                        }
                        fevents.AddRange(fe);
                    }

                    assignedUnits.Add((c.Unit, toTake, fevents));
                    remainingMunitions[c.Unit.Id] -= toTake;
                    laneNeeded[currentLane] -= toTake;
                    roundsCollected += toTake;
                }

                if (roundsCollected <= 0) {
                    plan.RejectReason ??= $"候选{candidates.Count}个，弹药需求{totalRoundsNeeded}发，实际收集0发";
                    plan.ThreatsUnengaged++; continue;
                }

                foreach (var (unit, rounds, fireEvents) in assignedUnits)
                {
                    plan.Assignments.Add(new UnitAssignment
                    {
                        FireUnitId = unit.Id,
                        DroneWaveId = threat.WaveId,
                        AmmoType = neededAmmo,
                        RoundsFired = rounds,
                        FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
                        FireEvents = fireEvents,
                    });
                    plan.MergedSchedule.AddRange(fireEvents);
                }

                plan.ThreatsEngaged++;
            }

            plan.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));

            // 按威胁汇总概率
            var threatProbs = new List<float>();
            foreach (var threat in sortedThreats)
            {
                var a2 = _ammoCatalog.FirstOrDefault(s =>
                    s.AerosolType == (int)MatchAmmo(_config, (PowerType)threat.Target.PowerType));
                int totalRounds = plan.Assignments
                    .Where(a => a.DroneWaveId == threat.WaveId)
                    .Sum(a => a.RoundsFired);
                if (totalRounds > 0)
                    threatProbs.Add(ComputeInterceptProbability(threat, a2, totalRounds, env));
            }
            plan.OverallProbability = threatProbs.Count > 0 ? threatProbs.Average() : 0f;

            // 失败原因 + 建议值
            var reasons = new List<string>();
            foreach (var threat in sortedThreats)
            {
                bool engaged = plan.Assignments.Any(a => a.DroneWaveId == threat.WaveId);
                if (engaged) continue;
                var ammo = MatchAmmo(_config, (PowerType)threat.Target.PowerType);
                var matching = fireUnits.Where(u => u.AmmoTypes.Contains(ammo)).ToList();
                if (matching.Count == 0)
                    reasons.Add($"威胁 {threat.WaveId}: 无火力单元装载 {ammo} 弹药");
                else
                {
                    var (_, expTime, _) = ComputeEffectiveRadius(_ammoCatalog.First(a => a.AerosolType == (int)ammo), env);
                    float avgSpd = GetDroneSpeedMs(threat);
                    float midArc = RouteGeometry.ArcLengthNearestTo(threat.Waypoints,
                        (float)(threat.Waypoints[0].PosX + threat.Waypoints[^1].PosX) / 2f,
                        (float)(threat.Waypoints[0].PosZ + threat.Waypoints[^1].PosZ) / 2f);
                    float travel = midArc - threat.DetectArc;
                    if (travel <= 0)
                    {
                        var (mx, _, mz) = RouteGeometry.PositionAt(threat.Waypoints, midArc);
                        float r = matching.Min(u => MathF.Sqrt((mx - u.Position.X) * (mx - u.Position.X) + (mz - u.Position.Z) * (mz - u.Position.Z)));
                        reasons.Add($"威胁 {threat.WaveId}: 探测边界在拦截点之后，建议探测范围≥{r:F0}m");
                    }
                    else if (travel / avgSpd <= expTime)
                    {
                        float maxDA = Math.Max(0, midArc - avgSpd * (expTime + _config.TimingSafetyMargin));
                        var (nx, _, nz) = RouteGeometry.PositionAt(threat.Waypoints, maxDA);
                        float r = matching.Min(u => MathF.Sqrt((nx - u.Position.X) * (nx - u.Position.X) + (nz - u.Position.Z) * (nz - u.Position.Z)));
                        reasons.Add($"威胁 {threat.WaveId}: 拦截窗口不足（{travel / avgSpd:F1}s<膨胀{expTime:F1}s），建议探测范围≥{r:F0}m（最晚弧长{maxDA:F0}m）");
                    }
                    else
                    {
                        string detail = plan.RejectReason != null ? $"（{plan.RejectReason}）" : "";
                        reasons.Add($"威胁 {threat.WaveId}: 候选存在但分配失败{detail}");
                    }
                }
            }
            plan.Summary = plan.ThreatsEngaged > 0
                ? $"分配 {plan.ThreatsEngaged} 个威胁，{plan.ThreatsUnengaged} 个无方案"
                    + (reasons.Count > 0 ? "。原因: " + string.Join("; ", reasons) : "")
                : "无威胁被分配拦截方案"
                    + (reasons.Count > 0 ? "。原因: " + string.Join("; ", reasons) : "");

            return plan;
        }

        // ═══════════════════════════════════════════════
        // 拦截窗口可行性检查
        // ═══════════════════════════════════════════════

        /// <summary>无人机速度（km/h），从航路第一个 waypoint 读取</summary>
        private static float GetDroneSpeedKph(DroneWave threat)
        {
            float spd = (float)threat.Waypoints[0].Speed;
            if (spd <= 0) throw new InvalidOperationException($"威胁 {threat.WaveId}: 航路点速度必须 > 0");
            return spd;
        }

        /// <summary>无人机速度（m/s）</summary>
        private static float GetDroneSpeedMs(DroneWave threat)
            => GetDroneSpeedKph(threat) / 3.6f;

        internal static bool HasInterceptWindow(DroneWave threat, float midArc,
            float expansionTime, float deliveryTime)
        {
            float travelArc = midArc - threat.DetectArc;
            if (travelArc <= 0) return false;
            float timeAvailable = travelArc / GetDroneSpeedMs(threat);
            return timeAvailable > expansionTime + deliveryTime;
        }

        // ═══════════════════════════════════════════════
        // 候选生成（使用真实物理）
        // ═══════════════════════════════════════════════

        private List<InterceptCandidate> GenerateCandidates(DroneWave threat,
            List<FireUnit> availableUnits, CombatScene env)
        {
            var candidates = new List<InterceptCandidate>();
            var neededAmmo = MatchAmmo(_config, (PowerType)threat.Target.PowerType);
            var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);

            var (ammoEff, expansionTime, _) = ComputeEffectiveRadius(ammo, env);
            var mid = ThreatMidpoint(threat);
            float midArc = RouteGeometry.ArcLengthNearestTo(threat.Waypoints, mid.X, mid.Z);

            foreach (var unit in availableUnits)
            {
                if (!unit.AmmoTypes.Contains(neededAmmo)) continue;

                if (unit.Type == PlatformType.AirBased)
                {
                    var c = BuildAirBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, expansionTime, mid, midArc, env);
                    if (c != null) candidates.Add(c);
                }
                else
                {
                    var c = BuildGroundBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, expansionTime, mid, midArc, env);
                    if (c != null) candidates.Add(c);
                }
            }

            return candidates;
        }

        private InterceptCandidate? BuildGroundBasedCandidate(DroneWave threat,
            FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
            float effectiveR, float expansionTime,
            Vector3 mid, float midArc, CombatScene env)
        {
            float dx = mid.X - unit.Position.X;
            float dz = mid.Z - unit.Position.Z;
            float dist = (float)Math.Sqrt(dx * dx + dz * dz);

            if (unit.MuzzleVelocity <= 0) return null;
            float maxRange = unit.MuzzleVelocity * unit.MuzzleVelocity / 9.81f;
            if (dist > maxRange) return null;

            float shellTime = dist / unit.MuzzleVelocity;
            if (!HasInterceptWindow(threat, midArc, expansionTime, shellTime)) return null;

            float interceptTime = threat.ArrivalTime;

            float avgSpeed = GetDroneSpeedMs(threat);
            float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType);
            float actualExposure = effectiveR * 2f / avgSpeed;
            float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);

            return new InterceptCandidate
            {
                Unit = unit,
                AmmoType = ammoType,
                EarliestInterceptTime = interceptTime,
                KillProbability = prob,
            };
        }

        private InterceptCandidate? BuildAirBasedCandidate(DroneWave threat,
            FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
            float effectiveR, float expansionTime,
            Vector3 mid, float midArc, CombatScene env)
        {
            if (unit.ReleaseAltitude <= 0 || unit.CruiseSpeed <= 0) return null;
            float releaseAlt = unit.ReleaseAltitude;
            float flightDist = Kinematics.Distance3D(
                unit.Position.X, unit.Position.Y, unit.Position.Z,
                mid.X, releaseAlt, mid.Z);
            float flightTime = flightDist / unit.CruiseSpeed;
            float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude);
            float deliveryTime = flightTime + fallTime;

            float interceptTime = threat.ArrivalTime;
            if (deliveryTime > interceptTime) return null;
            if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null;

            // 最小距离约束：无人机至少需要飞 vd*(R+expansion+fallTime) 才能被拦截
            float speedMs = GetDroneSpeedMs(threat);
            float minArc = threat.DetectArc + speedMs * (_config.ReactionTime + expansionTime + fallTime);
            float totalArc = RouteGeometry.TotalLength(threat.Waypoints);
            if (minArc >= totalArc) return null;

            var (icArc, _, _) = InterceptCalculator.ComputeHorizontal(
                threat.Waypoints, threat.DetectArc, GetDroneSpeedKph(threat),
                _config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed,
                unit.ReleaseAltitude, (float)threat.Target.TypicalAltitude);
            if (icArc <= 0) return null;

            float avgSpeed = GetDroneSpeedMs(threat);
            float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType);
            float actualExposure = effectiveR * 2f / avgSpeed;
            float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);

            return new InterceptCandidate
            {
                Unit = unit,
                AmmoType = ammoType,
                EarliestInterceptTime = interceptTime,
                KillProbability = prob,
            };
        }

        // ═══════════════════════════════════════════════
        // 弹药计算（使用 AmmunitionSpec + 环境参数）
        // ═══════════════════════════════════════════════

        private (float effectiveRadius, float expansionTime, float rPhase2) ComputeEffectiveRadius(
            AmmunitionSpec ammo, CombatScene env)
        {
            var model = new CloudExpansionModel(ammo, env);
            float rPhase2 = model.RadiusAt(30f);
            float expansionTime = model.TimeToReach(rPhase2);
            // 云团被穿过时的真实年龄 = expansionTime（云团生成后膨胀到有效半径的时长）。
            // 这是云团自身的物理量，与无人机飞行时间无关。
            float effectiveR = model.RadiusAt(expansionTime);
            return (effectiveR, expansionTime, rPhase2);
        }

        private int CalcRoundsNeeded(DroneWave threat, AmmunitionSpec ammo,
            CombatScene env, bool isAirBased, float turbulentRadius)
        {
            var cloudModel = new CloudExpansionModel(ammo, env);
            float avgSpeed = GetDroneSpeedMs(threat);
            float neededExposure = _damageModel.RequiredExposureSeconds(
                (TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, (AerosolType)ammo.AerosolType);
            float requiredCoverage = neededExposure * avgSpeed;
            // 间距由配置的重叠比例驱动，公式在 CloudExpansionModel（共享）
            float spacing = 2f * turbulentRadius * (1f - _config.CloudOverlapRatio);
            return cloudModel.RoundsNeeded(requiredCoverage, spacing);
        }

        private float ComputeInterceptProbability(DroneWave threat,
            AmmunitionSpec ammo, int rounds, CombatScene env)
        {
            float avgSpeed = GetDroneSpeedMs(threat);
            var (effectiveR, _, _) = ComputeEffectiveRadius(ammo, env);
            float spacing = 2f * effectiveR;
            float actualCoverage = spacing * (rounds - 1) + 2f * effectiveR;
            float actualExposure = actualCoverage / avgSpeed;

            var aerosolType = (AerosolType)ammo.AerosolType;
            float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, aerosolType);

            return Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
        }

        // ═══════════════════════════════════════════════
        // 发射事件生成（真实物理）
        // ═══════════════════════════════════════════════

        private (List<FireEvent> Events, string? RejectReason) TryGenerateFireEvents(
            DroneWave threat, FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
            CombatScene env, float targetOffset, int yLane, int yLanes, float formationWidth)
        {
            var events = new List<FireEvent>();
            var wps = threat.Waypoints;
            if (wps == null || wps.Count < 2) return (events, "航路无效");

            var cloudModel = new CloudExpansionModel(ammo, env);
            // 云龄 = expansionTime（云团生成后膨胀到有效半径的时长）
            float effectiveAge = cloudModel.TimeToReach(cloudModel.TurbulentRadius) * _config.ExpansionFactor;
            float effectiveR = cloudModel.RadiusAt(effectiveAge);
            float expansionTime = cloudModel.TimeToReach(effectiveR);

            float densityAtPassage = cloudModel.DensityAt(expansionTime);
            if (densityAtPassage < (float)ammo.EffectiveConcentration)
                return (events, $"云团密度{densityAtPassage:E2}<有效阈值{ammo.EffectiveConcentration:E2}");

            float typicalSpeed = GetDroneSpeedKph(threat);
            if (typicalSpeed <= 0) return (events, "目标速度无效");

            // 用 InterceptCalculator 计算拦截弧长和发射角
            var mid = ThreatMidpoint(threat);
            float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z);
            float crossArc;
            float launchAngle = 0;
            float interceptShellTime = 0;
            if (unit.Type == PlatformType.GroundBased)
            {
                var (ia, st, la) = InterceptCalculator.Compute(
                    wps, threat.DetectArc, typicalSpeed,
                    _config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
                crossArc = (ia > 0 ? ia : midArc) + targetOffset;
                launchAngle = la;
                interceptShellTime = st;
            }
            else
            {
                var (ia, _, la) = InterceptCalculator.ComputeHorizontal(
                    wps, threat.DetectArc, typicalSpeed,
                    _config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed,
                    unit.Position.Y, (float)threat.Target.TypicalAltitude);
                crossArc = ia;
                launchAngle = la;
            }

            // 编队横向偏移：按车道分布在航路法向上，与 DroneEntity 编队偏移一致（起点展开）。
            // DroneEntity: latOffset = formationIndex * lateralSpacing（0/50/100...）
            // 法向 = 切向旋转 90°，用 RouteGeometry.TangentAt 取穿越点处航路方向
            var (tanX, tanZ) = RouteGeometry.TangentAt(wps, crossArc);
            float normalX = -tanZ, normalZ = tanX; // 切向逆时针 90° = 左侧法向
            float laneSpacing = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f;
            float laneOffset = yLane * laneSpacing;

            // 穿越点 = 航路上 crossArc 处 + 横向车道偏移
            var (routeX, routeY, routeZ) = RouteGeometry.PositionAt(wps, crossArc);
            float tx = routeX + normalX * laneOffset;
            float ty = routeY;
            float tz = routeZ + normalZ * laneOffset;

            // 无人机到达穿越点的时间：基于探测边界，而非航路起点。
            // planner 是参谋，只能基于探测信息规划。威胁从探测边界被发现，
            // 飞行时间 = (拦截弧长 - 探测弧长) / 速度。无探测时 DetectArc=0（回退到起点）。
            float travelArc = crossArc - threat.DetectArc;
            if (travelArc < 0) return (events, $"探测边界在拦截点之后（{threat.DetectArc:F0}m≥{crossArc:F0}m）");
            float txArrival = RouteGeometry.TravelTimeTo(wps, travelArc, typicalSpeed);
            float recommendedTiming = txArrival - expansionTime;
            if (recommendedTiming <= 0f) return (events, $"膨胀{expansionTime:F1}s≥到达{txArrival:F1}s");

            // 风偏补偿：云团生成后会被风吹偏，补偿时长 = 从生成到被穿过。
            // 每朵云的生成时刻不同（受发射/飞行时间影响），但 planner 此处用 expansionTime
            // 作为云团从生成到被穿过的时长（recommendedTiming 已对齐），各发独立用各自的 expansionTime。
            var (wx, _, wz) = Kinematics.WindToVector((WindDirection)env.WindDirection, (float)env.WindSpeed);
            float cloudGenX = tx - wx * expansionTime;
            float cloudGenZ = tz - wz * expansionTime;

            float fireTime;
            float deliveryTime;
            if (unit.Type == PlatformType.AirBased)
            {
                if (unit.CruiseSpeed <= 0)
                    throw new InvalidOperationException($"空基单元 {unit.Id}: CruiseSpeed={unit.CruiseSpeed} 必须>0");

                float targetAlt = (float)threat.Target.TypicalAltitude;
                float heightDiff = targetAlt - unit.Position.Y;
                if (heightDiff >= 0)
                    return (events, $"空基单元 {unit.Id}: 平台高度({unit.Position.Y:F0})≤目标高度({targetAlt:F0})，无法重力下落");

                // 弹药从固定阵位水平射出，重力下落到目标高度
                var (horizontalRange, fallTime) = Kinematics.ComputeParabolicRange(unit.CruiseSpeed, 0, heightDiff);
                deliveryTime = fallTime;
                fireTime = recommendedTiming - fallTime;

                // 云团初始位置 = 炮弹到达位置（飞行方向指向拦截点）
                float dx = tx - unit.Position.X;
                float dz = tz - unit.Position.Z;
                float dist = (float)Math.Sqrt(dx * dx + dz * dz);
                if (dist > 0.001f)
                {
                    cloudGenX = unit.Position.X + dx / dist * horizontalRange;
                    cloudGenZ = unit.Position.Z + dz / dist * horizontalRange;
                }
            }
            else
            {
                if (unit.MuzzleVelocity <= 0)
                    throw new InvalidOperationException($"地基单元 {unit.Id}: MuzzleVelocity={unit.MuzzleVelocity} 必须>0");
                // 使用 InterceptCalculator 前向计算的时间，不再用 ParabolicShellTime 反算
                if (interceptShellTime <= 0)
                    return (events, "拦截计算未得出有效飞行时间");
                deliveryTime = interceptShellTime;
                fireTime = recommendedTiming - interceptShellTime;
            }

            if (fireTime <= 0f)
                return (events, $"发射时机{fireTime:F1}s≤0（到达{deliveryTime:F1}s>窗口{recommendedTiming:F1}s）");

            events.Add(new FireEvent
            {
                FireTime = fireTime,
                PlatformIndex = 0,
                TargetX = cloudGenX,
                TargetY = ty,
                TargetZ = cloudGenZ,
                MuzzleVelocity = unit.Type == PlatformType.AirBased ? unit.CruiseSpeed : unit.MuzzleVelocity,
                LaunchAngle = launchAngle,
            });

            return (events, null);
        }

        // ═══════════════════════════════════════════════
        // Step 5: 临界方案（概率阈值 50%）
        // ═══════════════════════════════════════════════

        private DefensePlan DeriveCritical(DefensePlan best)
        {
            var critical = new DefensePlan
            {
                ThreatsEngaged = best.ThreatsEngaged,
                ThreatsUnengaged = best.ThreatsUnengaged,
            };

            foreach (var assignment in best.Assignments)
            {
                int rounds = assignment.RoundsFired;
                while (rounds > 1)
                {
                    // 简化：弹药减半 → 概率减半
                    if ((float)rounds / assignment.RoundsFired < _config.CriticalProbabilityThreshold) break;
                    rounds--;
                }

                var reduced = new UnitAssignment
                {
                    FireUnitId = assignment.FireUnitId,
                    DroneWaveId = assignment.DroneWaveId,
                    AmmoType = assignment.AmmoType,
                    RoundsFired = rounds,
                    FirstFireTime = assignment.FirstFireTime,
                    FireEvents = assignment.FireEvents.Take(rounds).ToList(),
                };
                critical.Assignments.Add(reduced);
                critical.MergedSchedule.AddRange(reduced.FireEvents);
            }

            critical.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
            critical.OverallProbability = _config.CriticalProbabilityThreshold;
            critical.Summary = $"临界方案：刚好满足 50% 拦截概率";
            return critical;
        }

        // ═══════════════════════════════════════════════
        // 辅助
        // ═══════════════════════════════════════════════

        private static Vector3 ThreatMidpoint(DroneWave threat)
        {
            if (threat.Waypoints.Count < 2)
                return new Vector3(0, (float)threat.Target.TypicalAltitude, 0);
            var s = threat.Waypoints[0];
            var e = threat.Waypoints[^1];
            return new Vector3(
                (float)(s.PosX + e.PosX) / 2f,
                (float)(s.PosY + e.PosY) / 2f,
                (float)(s.PosZ + e.PosZ) / 2f);
        }
    }
}
