using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Commands;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Core.Config;
using NavisworksTransport.PathPlanning;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
using Newtonsoft.Json;

namespace NavisworksTransport.Core.Services
{
        /// <summary>
        /// 本地测试自动化 HTTP 控制面。
        /// 当前暴露最小只读/导出接口，后续可在保持协议稳定的前提下追加命令端点。
        /// </summary>
    public sealed class TestAutomationHttpService : IDisposable
    {
        private const int DefaultPort = 18777;
        private const string DefaultHost = "127.0.0.1";
        private const string PortEnvironmentVariable = "TRANSPORTPLUGIN_TEST_PORT";
        private const string DefaultAutoTestRoutePrefix = "自动测试_";
        private const string AutoTestRoutesResourceFileName = "auto-test-routes.xml";

        // 多实例支持：每个 Navisworks 实例通过环境变量 TRANSPORTPLUGIN_TEST_PORT 分配独立端口（默认 18777）。
        private int _resolvedPort;
        private bool _portResolved;

        private static readonly Lazy<TestAutomationHttpService> _instance =
            new Lazy<TestAutomationHttpService>(() => new TestAutomationHttpService());

        private readonly object _syncRoot = new object();
        private static int _autoConfirmCollisionAnalysisDialogRequestCount;
        private static int _autoChooseCreateNewDetectionRecordRequestCount;

        // 测试执行串行锁：动画/路径/碰撞类请求共享 UI 线程与单例动画管理器，
        // 客户端超时断开后服务端任务仍在运行，并发会污染 UI 状态（如物体选择串扰），必须串行。
        private static readonly SemaphoreSlim _testExecutionLock = new SemaphoreSlim(1, 1);

        private TcpListener _listener;
        private CancellationTokenSource _cts;
        private Task _acceptLoopTask;
        private DateTime _startedAtUtc;
        private string _lastStartError;

        private TestAutomationHttpService()
        {
        }

        public static TestAutomationHttpService Instance => _instance.Value;

        public bool IsRunning
        {
            get
            {
                lock (_syncRoot)
                {
                    return _listener != null;
                }
            }
        }

        public int Port => ResolvePort();

        public string BaseUrl => $"http://{DefaultHost}:{Port}";

        public static bool ShouldAutoConfirmCollisionAnalysisDialogs =>
            Interlocked.CompareExchange(ref _autoConfirmCollisionAnalysisDialogRequestCount, 0, 0) > 0;

        public static bool ShouldAutoChooseCreateNewDetectionRecord =>
            Interlocked.CompareExchange(ref _autoChooseCreateNewDetectionRecordRequestCount, 0, 0) > 0;

        public void Start()
        {
            lock (_syncRoot)
            {
                if (_listener != null)
                {
                    return;
                }

                try
                {
                    _cts = new CancellationTokenSource();
                    _listener = new TcpListener(IPAddress.Loopback, ResolvePort());
                    _listener.Start();
                    _startedAtUtc = DateTime.UtcNow;
                    _lastStartError = null;
                    _acceptLoopTask = Task.Run(() => AcceptLoopAsync(_cts.Token));

                    LogManager.Info($"[测试HTTP] 服务已启动: {BaseUrl}");
                }
                catch (Exception ex)
                {
                    _lastStartError = ex.Message;
                    _listener = null;
                    _cts?.Dispose();
                    _cts = null;
                    _acceptLoopTask = null;
                    LogManager.Error($"[测试HTTP] 服务启动失败: {ex.Message}", ex);
                }
            }
        }

        public void Stop()
        {
            Task acceptLoopTask = null;

            lock (_syncRoot)
            {
                if (_listener == null)
                {
                    return;
                }

                try
                {
                    _cts?.Cancel();
                    _listener.Stop();
                    acceptLoopTask = _acceptLoopTask;
                }
                catch (Exception ex)
                {
                    LogManager.Warning($"[测试HTTP] 服务停止时出现警告: {ex.Message}");
                }
                finally
                {
                    _listener = null;
                    _acceptLoopTask = null;
                    _cts?.Dispose();
                    _cts = null;
                }
            }

            try
            {
                acceptLoopTask?.Wait(1000);
            }
            catch
            {
                // 忽略退出等待异常
            }

            LogManager.Info("[测试HTTP] 服务已停止");
        }

        private async Task AcceptLoopAsync(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                TcpClient client = null;
                try
                {
                    client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false);
                    _ = Task.Run(() => HandleClientAsync(client, cancellationToken), cancellationToken);
                }
                catch (ObjectDisposedException)
                {
                    break;
                }
                catch (SocketException) when (cancellationToken.IsCancellationRequested)
                {
                    break;
                }
                catch (Exception ex)
                {
                    LogManager.Error($"[测试HTTP] 接收请求失败: {ex.Message}", ex);
                    client?.Dispose();
                }
            }
        }

        private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
        {
            using (client)
            using (var stream = client.GetStream())
            using (var reader = new StreamReader(stream, Encoding.UTF8, false, 4096, true))
            using (var writer = new StreamWriter(stream, new UTF8Encoding(false), 4096, true) { NewLine = "\r\n", AutoFlush = true })
            {
                try
                {
                    string requestLine = await reader.ReadLineAsync().ConfigureAwait(false);
                    if (string.IsNullOrWhiteSpace(requestLine))
                    {
                        return;
                    }

                    string[] requestLineParts = requestLine.Split(' ');
                    if (requestLineParts.Length < 2)
                    {
                        await WriteJsonResponseAsync(writer, 400, BuildEnvelope(false, error: "Invalid request line")).ConfigureAwait(false);
                        return;
                    }

                    string method = requestLineParts[0].Trim().ToUpperInvariant();
                    string requestTarget = requestLineParts[1].Trim();
                    var request = ParseRequestTarget(requestTarget);

                    string headerLine;
                    while (!string.IsNullOrEmpty(headerLine = await reader.ReadLineAsync().ConfigureAwait(false)))
                    {
                        if (cancellationToken.IsCancellationRequested)
                        {
                            return;
                        }
                    }

                    if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/ping", StringComparison.OrdinalIgnoreCase))
                    {
                        var payload = new
                        {
                            ok = true,
                            service = "NavisworksTransport.TestAutomation",
                            protocol = "http",
                            version = 1,
                            baseUrl = BaseUrl,
                            serverTimeUtc = DateTime.UtcNow.ToString("o")
                        };
                        await WriteJsonResponseAsync(writer, 200, payload).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/status", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(BuildStatusPayload);
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/routes", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(BuildRoutesPayload);
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/route-grid-diagnostics", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => BuildRouteGridDiagnosticsPayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/selection", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(BuildSelectionPayload);
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/list-model-items", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => BuildModelItemsPayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/detection-record", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => BuildDetectionRecordPayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/analyze-path", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => AnalyzePathPayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/batch-queue-add", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = await BatchQueueAddAsync(request.Query).ConfigureAwait(false);
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/batch-queue-process", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = BatchQueueProcessPayload(request.Query);
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/edit-route", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => EditRoutePayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/export-section-box", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => ExportSectionBoxPayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/export-debug-snapshot", StringComparison.OrdinalIgnoreCase))
                    {
                        object snapshotPayload = InvokeOnUiThread(BuildDebugSnapshotPayload);
                        string snapshotFilePath = WriteSnapshotToDisk(snapshotPayload);
                        var payload = new
                        {
                            snapshotFilePath,
                            snapshot = snapshotPayload
                        };

                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/select-route", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => SelectRoutePayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/select-default-route", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => SelectDefaultRoutePayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/select-animated-object", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => SelectAnimatedObjectPayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/run-ground-collision-test", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = await RunVirtualCollisionTestAsync(request.Query, PathType.Ground).ConfigureAwait(false);
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/run-virtual-collision-test", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = await RunVirtualCollisionTestAsync(request.Query, null).ConfigureAwait(false);
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/analyze-auto-path-grid", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => AnalyzeAutoPathGridPayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/run-auto-path", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => RunAutoPathPayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/import-route-file", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => ImportRouteFilePayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/export-route-file", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = InvokeOnUiThread(() => ExportRouteFilePayload(request.Query));
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/probe-animation-frames", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = await ProbeAnimationFramesAsync(request.Query).ConfigureAwait(false);
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
                        string.Equals(request.Path, "/api/test/animation-playback-control", StringComparison.OrdinalIgnoreCase))
                    {
                        object payload = await AnimationPlaybackControlAsync(request.Query).ConfigureAwait(false);
                        await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
                        return;
                    }

                    if (!string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
                        !string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase))
                    {
                        await WriteJsonResponseAsync(writer, 405, BuildEnvelope(false, error: "Only GET and POST are supported in the current test API")).ConfigureAwait(false);
                        return;
                    }

                    await WriteJsonResponseAsync(writer, 404, BuildEnvelope(false, error: $"Unknown endpoint: {request.Path}")).ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    LogManager.Error($"[测试HTTP] 处理请求失败: {ex.Message}", ex);
                    await WriteJsonResponseAsync(writer, 500, BuildEnvelope(false, error: ex.Message)).ConfigureAwait(false);
                }
            }
        }

        private object BuildStatusPayload()
        {
            Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
            PathPlanningManager pathManager = PathPlanningManager.GetActivePathManager();
            PathAnimationManager animationManager = PathAnimationManager.GetInstance();

            PathRoute currentRoute = pathManager?.CurrentRoute;
            List<PathRoute> allRoutes = pathManager?.GetAllRoutes();

            return new
            {
                service = new
                {
                    name = "NavisworksTransport.TestAutomation",
                    protocol = "http",
                    version = 1,
                    isRunning = IsRunning,
                    baseUrl = BaseUrl,
                    port = Port,
                    startedAtUtc = _startedAtUtc == default(DateTime) ? null : _startedAtUtc.ToString("o"),
                    lastStartError = _lastStartError
                },
                environment = new
                {
                    processId = System.Diagnostics.Process.GetCurrentProcess().Id,
                    machineName = Environment.MachineName,
                    logFilePath = LogManager.LogFilePath
                },
                document = new
                {
                    hasActiveDocument = activeDocument != null,
                    fileName = activeDocument?.FileName,
                    title = string.IsNullOrEmpty(activeDocument?.FileName) ? null : Path.GetFileName(activeDocument.FileName),
                    modelCount = activeDocument?.Models?.Count ?? 0
                },
                pathManager = new
                {
                    isAvailable = pathManager != null,
                    isDatabaseReady = pathManager != null && pathManager.IsDatabaseReady,
                    routeCount = allRoutes?.Count ?? 0,
                    currentRouteId = currentRoute?.Id,
                    currentRouteName = currentRoute?.Name,
                    currentRoutePathType = currentRoute?.PathType.ToString(),
                    currentRoutePointCount = currentRoute?.Points?.Count ?? 0
                },
                animation = new
                {
                    isAvailable = animationManager != null,
                    currentState = animationManager?.CurrentState.ToString(),
                    isAnimating = animationManager?.IsAnimating ?? false,
                    currentFrame = animationManager?.CurrentFrame ?? 0,
                    totalFrames = animationManager?.TotalFrames ?? 0,
                    hasTrackedRotation = animationManager?.HasTrackedRotation ?? false,
                    currentYawDegrees = animationManager == null ? 0.0 : animationManager.CurrentYaw * 180.0 / Math.PI
                }
            };
        }

        private object BuildDebugSnapshotPayload()
        {
            Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
            PathPlanningManager pathManager = PathPlanningManager.GetActivePathManager();
            PathAnimationManager animationManager = PathAnimationManager.GetInstance();

            PathRoute currentRoute = pathManager?.CurrentRoute;
            ModelItem controlledObject = ResolveControlledObject(animationManager);
            var trackedState = ResolveTrackedState(animationManager, controlledObject);
            var geometryState = ResolveGeometryState(controlledObject);
            var boundingBoxState = ResolveBoundingBoxState(controlledObject);

            return new
            {
                snapshotVersion = 1,
                capturedAtUtc = DateTime.UtcNow.ToString("o"),
                document = new
                {
                    hasActiveDocument = activeDocument != null,
                    fileName = activeDocument?.FileName,
                    title = string.IsNullOrEmpty(activeDocument?.FileName) ? null : Path.GetFileName(activeDocument.FileName),
                    modelCount = activeDocument?.Models?.Count ?? 0
                },
                route = new
                {
                    isAvailable = currentRoute != null,
                    id = currentRoute?.Id,
                    name = currentRoute?.Name,
                    pathType = currentRoute?.PathType.ToString(),
                    pointCount = currentRoute?.Points?.Count ?? 0
                },
                animation = new
                {
                    isAvailable = animationManager != null,
                    currentState = animationManager?.CurrentState.ToString(),
                    isAnimating = animationManager?.IsAnimating ?? false,
                    currentFrame = animationManager?.CurrentFrame ?? 0,
                    totalFrames = animationManager?.TotalFrames ?? 0,
                    currentYawDegrees = animationManager == null ? 0.0 : animationManager.CurrentYaw * 180.0 / Math.PI,
                    hasTrackedRotation = animationManager?.HasTrackedRotation ?? false
                },
                controlledObject = new
                {
                    exists = controlledObject != null,
                    displayName = controlledObject?.DisplayName,
                    instanceGuid = controlledObject == null ? null : controlledObject.InstanceGuid.ToString(),
                    isVirtualObject = controlledObject != null &&
                                      VirtualObjectManager.Instance.IsVirtualObjectActive &&
                                      ReferenceEquals(VirtualObjectManager.Instance.CurrentVirtualObject, controlledObject),
                    trackedState,
                    geometryState,
                    boundingBox = boundingBoxState
                },
                environment = new
                {
                    processId = System.Diagnostics.Process.GetCurrentProcess().Id,
                    machineName = Environment.MachineName,
                    logFilePath = LogManager.LogFilePath
                }
            };
        }

        private object BuildRoutesPayload()
        {
            PathPlanningManager pathManager = PathPlanningManager.GetActivePathManager();
            List<PathRoute> routes = pathManager?.GetAllRoutes() ?? new List<PathRoute>();
            PathRoute currentRoute = pathManager?.CurrentRoute;

            return new
            {
                isAvailable = pathManager != null,
                currentRouteId = currentRoute?.Id,
                currentRouteName = currentRoute?.Name,
                currentRoutePathType = currentRoute?.PathType.ToString(),
                totalRouteCount = routes.Count,
                routes = routes.Select(route => SerializeRoute(route, currentRoute)).ToList(),
                suggestedAutoTestRoutes = new
                {
                    ground = TryFindAutoTestRoute(routes, PathType.Ground)?.Name,
                    hoisting = TryFindAutoTestRoute(routes, PathType.Hoisting)?.Name,
                    rail = TryFindAutoTestRoute(routes, PathType.Rail)?.Name
                }
            };
        }

        /// <summary>
        /// 枚举模型叶子物体：DisplayName + ModelPath(PathId) + 祖先名链。
        /// 供集成测试动态定位真实物体（ModelPath 跨会话不稳定，测试不应硬编码 PathId）。
        /// </summary>
        private object BuildModelItemsPayload(Dictionary<string, string> query)
        {
            Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
            if (activeDocument == null)
            {
                throw new InvalidOperationException("当前没有活动文档");
            }

            string nameFilter = GetOptionalQueryValue(query, "name");
            int maxResults = 200;
            if (int.TryParse(GetOptionalQueryValue(query, "maxResults"), out int parsedMax) && parsedMax > 0)
            {
                maxResults = Math.Min(parsedMax, 500);
            }

            var items = new List<object>();
            foreach (Model model in activeDocument.Models)
            {
                if (model?.RootItem == null)
                {
                    continue;
                }

                foreach (ModelItem item in model.RootItem.DescendantsAndSelf)
                {
                    if (item == null)
                    {
                        continue;
                    }

                    // 仅枚举有显示名的节点（叶子几何节点 DisplayName 多为空，名字在层级节点上）
                    if (string.IsNullOrWhiteSpace(item.DisplayName))
                    {
                        continue;
                    }

                    if (!string.IsNullOrWhiteSpace(nameFilter) &&
                        item.DisplayName.IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) < 0 &&
                        BuildAncestorNameChain(item).IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) < 0)
                    {
                        continue;
                    }

                    string pathId = null;
                    try
                    {
                        var path = activeDocument.Models.CreatePathId(item);
                        pathId = path.PathId;
                    }
                    catch
                    {
                        // 忽略路径解析失败
                    }

                    // 仅在有名字过滤时计算包围盒中心（枚举全量时避免性能开销）
                    object boundingBoxCenter = null;
                    if (!string.IsNullOrWhiteSpace(nameFilter))
                    {
                        try
                        {
                            var bbox = item.BoundingBox();
                            boundingBoxCenter = new
                            {
                                x = bbox.Center.X,
                                y = bbox.Center.Y,
                                z = bbox.Center.Z
                            };
                        }
                        catch
                        {
                            // 忽略包围盒解析失败
                        }
                    }

                    items.Add(new
                    {
                        displayName = item.DisplayName,
                        instanceGuid = item.InstanceGuid.ToString(),
                        pathId,
                        boundingBoxCenter,
                        ancestorChain = BuildAncestorNameChain(item)
                    });

                    if (items.Count >= maxResults)
                    {
                        break;
                    }
                }
            }

            return new
            {
                totalCount = items.Count,
                maxResults,
                nameFilter,
                items
            };
        }

        private static string BuildAncestorNameChain(ModelItem item)
        {
            try
            {
                var segments = new List<string>();
                var current = item;
                int guard = 0;

                while (current != null && guard < 15)
                {
                    var name = current.DisplayName;
                    if (!string.IsNullOrWhiteSpace(name))
                    {
                        segments.Add(name.Trim());
                    }
                    current = current.Parent;
                    guard++;
                }

                segments.Reverse();
                return string.Join(" / ", segments);
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// 查询碰撞检测记录（集成测试验证检测记录持久化：动画参数/关联路径正确落库）。
        /// </summary>
        /// <summary>
        /// 路径分析（效率/安全评分），集成测试验证分析链路与评分结构。
        /// </summary>
        /// <summary>
        /// 剖面盒导出（T16）：mode=bbox 用指定包围盒导出 NWD（集成测试可控，不依赖 UI 剖面盒状态）；
        /// mode=activeView 用当前视图剖面盒（需 UI 已激活 Box 模式）。
        /// </summary>
        private static object ExportSectionBoxPayload(Dictionary<string, string> query)
        {
            Document document = Autodesk.Navisworks.Api.Application.ActiveDocument;
            if (document == null)
            {
                throw new InvalidOperationException("当前没有活动文档");
            }

            string mode = GetOptionalQueryValue(query, "mode") ?? "bbox";
            string filePath = GetOptionalQueryValue(query, "path");
            if (string.IsNullOrWhiteSpace(filePath))
            {
                filePath = Path.Combine(Path.GetTempPath(), $"sectionbox-export-{Guid.NewGuid():N}.nwd");
            }

            var exporter = new SectionBoxExporter();

            if (string.Equals(mode, "bbox", StringComparison.OrdinalIgnoreCase))
            {
                double x1 = ParseRequiredDouble(query, "x1");
                double y1 = ParseRequiredDouble(query, "y1");
                double z1 = ParseRequiredDouble(query, "z1");
                double x2 = ParseRequiredDouble(query, "x2");
                double y2 = ParseRequiredDouble(query, "y2");
                double z2 = ParseRequiredDouble(query, "z2");

                var bounds = new BoundingBox3D(
                    new Point3D(Math.Min(x1, x2), Math.Min(y1, y2), Math.Min(z1, z2)),
                    new Point3D(Math.Max(x1, x2), Math.Max(y1, y2), Math.Max(z1, z2)));

                var traversal = exporter.GetObjectsAndHiddenItems(document, bounds);
                if (traversal.ObjectsInBox.Count == 0)
                {
                    throw new InvalidOperationException("指定包围盒内没有找到对象");
                }

                string exportedPath = exporter.ExportToNwd(document, traversal, filePath);
                if (string.IsNullOrEmpty(exportedPath) || !File.Exists(exportedPath))
                {
                    throw new InvalidOperationException($"NWD 导出失败: {filePath}");
                }

                LogManager.Info($"[测试HTTP] 剖面盒导出完成(bbox): 对象={traversal.ObjectsInBox.Count}, 隐藏={traversal.ItemsToHide.Count}, 文件={exportedPath}");

                return new
                {
                    mode = "bbox",
                    filePath = exportedPath,
                    objectCount = traversal.ObjectsInBox.Count,
                    hiddenNodeCount = traversal.ItemsToHide.Count,
                    fileSize = new FileInfo(exportedPath).Length
                };
            }

            // activeView 模式：依赖当前视图剖面盒
            var result = exporter.ExportSectionBoxFromActiveView(document, filePath);
            if (!result.Success)
            {
                throw new InvalidOperationException(result.ErrorMessage ?? "剖面盒导出失败");
            }

            return new
            {
                mode = "activeView",
                filePath = result.FilePath,
                objectCount = result.ObjectCount,
                hiddenNodeCount = result.HiddenNodeCount,
                fileSize = result.FileSize
            };
        }

        /// <summary>
        /// 路径编辑集成测试端点（T11）：action=remove-point/update-point/orthogonalize/remove-loops。
        /// 直接操作临时测试路径，验证 PathPlanningManager 编辑链路。
        /// </summary>
        private static object EditRoutePayload(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            string action = GetRequiredQueryValue(query, "action");
            string routeName = GetRequiredQueryValue(query, "routeName");

            PathRoute route = pathManager
                .GetAllRoutes()
                ?.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
            if (route == null)
            {
                throw new InvalidOperationException($"找不到指定路径: {routeName}");
            }

            List<PathPoint> orderedPoints = route.Points?.OrderBy(p => p.Index).ToList() ?? new List<PathPoint>();

            switch (action.ToLowerInvariant())
            {
                case "remove-point":
                {
                    int pointIndex = ParseRequiredInt(query, "pointIndex");
                    if (pointIndex < 0 || pointIndex >= orderedPoints.Count)
                    {
                        throw new InvalidOperationException($"路径点索引越界: {pointIndex}");
                    }

                    // 真正的删点走 PathRoute.RemovePoint（更新数据 + 重算 + 入库），
                    // RemovePathPoint 仅处理 3D 标记可视化
                    if (!route.RemovePoint(orderedPoints[pointIndex]))
                    {
                        throw new InvalidOperationException($"删除路径点失败: {routeName}[{pointIndex}]");
                    }
                    break;
                }

                case "update-point":
                {
                    int pointIndex = ParseRequiredInt(query, "pointIndex");
                    double x = ParseRequiredDouble(query, "x");
                    double y = ParseRequiredDouble(query, "y");
                    double z = ParseRequiredDouble(query, "z");
                    if (pointIndex < 0 || pointIndex >= orderedPoints.Count)
                    {
                        throw new InvalidOperationException($"路径点索引越界: {pointIndex}");
                    }

                    var newPosition = new Point3D(x, y, z);
                    if (!pathManager.UpdatePathPointWithConstraints(route, pointIndex, newPosition))
                    {
                        throw new InvalidOperationException($"更新路径点失败: {routeName}[{pointIndex}]");
                    }
                    break;
                }

                case "orthogonalize":
                {
                    if (!pathManager.OrthogonalizePath(route))
                    {
                        throw new InvalidOperationException($"路径正交化失败: {routeName}");
                    }
                    break;
                }

                case "remove-loops":
                {
                    // 无矩形环可去（返回 false）属正常情况，不视为失败
                    pathManager.RemoveRectangularLoops(route);
                    break;
                }

                default:
                    throw new InvalidOperationException($"未知编辑动作: {action}（支持 remove-point/update-point/orthogonalize/remove-loops）");
            }

            LogManager.Info($"[测试HTTP] 路径编辑完成: action={action}, route={routeName}");

            List<PathPoint> updatedPoints = route.Points?.OrderBy(p => p.Index).ToList() ?? new List<PathPoint>();
            return new
            {
                action,
                routeName,
                pointCount = updatedPoints.Count,
                points = updatedPoints.Select(point => new
                {
                    index = point.Index,
                    name = point.Name,
                    type = point.Type.ToString(),
                    position = SerializePoint3D(point.Position)
                }).ToList()
            };
        }

        private static int ParseRequiredInt(Dictionary<string, string> query, string key)
        {
            string raw = GetRequiredQueryValue(query, key);
            if (!int.TryParse(raw, out int value))
            {
                throw new InvalidOperationException($"无法解析整数参数 {key}: {raw}");
            }
            return value;
        }

        private static double ParseRequiredDouble(Dictionary<string, string> query, string key)
        {
            string raw = GetRequiredQueryValue(query, key);
            if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out double value))
            {
                throw new InvalidOperationException($"无法解析数值参数 {key}: {raw}");
            }
            return value;
        }

        /// <summary>
        /// 批量任务队列添加（集成测试验证队列管理，不执行 ProcessQueueAsync 以免长时间批量动画）。
        /// query：routeName（必填）、frameRate、durationSeconds。
        /// </summary>
        /// <summary>
        /// 触发批处理队列执行（F1）：后台执行，立即返回（ProcessQueueAsync 含模态进度条，
        /// 同步等待会阻塞 HTTP 响应；调用方轮询数据库 BatchQueueItems 状态跟踪）。
        /// </summary>
        private static object BatchQueueProcessPayload(Dictionary<string, string> query)
        {
            var manager = BatchQueueManager.Instance;
            manager.SetPathPlanningManager(RequirePathManager());

            LogManager.Info("[测试HTTP] 开始后台处理批处理队列（F1）");
            Task.Run(async () =>
            {
                try
                {
                    await manager.ProcessQueueAsync().ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    LogManager.Error($"[测试HTTP] 后台批处理执行异常: {ex.Message}", ex);
                }
            });

            return new
            {
                started = true,
                queueCount = manager.QueueCount,
                isExecuting = manager.IsExecuting
            };
        }

        private static async Task<object> BatchQueueAddAsync(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            string routeName = GetRequiredQueryValue(query, "routeName");

            PathRoute route = pathManager
                .GetAllRoutes()
                ?.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
            if (route == null)
            {
                throw new InvalidOperationException($"找不到指定路径: {routeName}");
            }

            int frameRate = 15;
            if (int.TryParse(GetOptionalQueryValue(query, "frameRate"), out int parsedFrameRate) && parsedFrameRate > 0)
            {
                frameRate = parsedFrameRate;
            }

            double durationSeconds = 5.0;
            if (double.TryParse(GetOptionalQueryValue(query, "durationSeconds"), NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedDuration) &&
                parsedDuration > 0)
            {
                durationSeconds = parsedDuration;
            }

            bool isVirtualObject = ParseBooleanQuery(query, "isVirtualObject", true);
            string movingObjectName = GetOptionalQueryValue(query, "movingObjectName");

            var manager = BatchQueueManager.Instance;
            manager.SetPathPlanningManager(pathManager);

            var item = new NavisworksTransport.Core.Models.BatchQueueItem
            {
                RouteId = route.Id,
                PathRouteName = route.Name,
                PathType = route.PathType,
                Status = NavisworksTransport.Core.Models.BatchQueueStatus.Pending,
                CreatedTime = DateTime.Now,
                FrameRate = frameRate,
                DurationSeconds = durationSeconds,
                IsVirtualObject = isVirtualObject,
                MovingObjectName = isVirtualObject ? null : movingObjectName,
                // 虚拟物体尺寸（米 → 模型单位）；DetectAllObjects 保持默认 true（F1 剖面盒自动检测场景）
                VirtualObjectLength = isVirtualObject ? 0.4 * UnitsConverter.GetMetersToUnitsConversionFactor() : 0,
                VirtualObjectWidth = isVirtualObject ? 0.4 * UnitsConverter.GetMetersToUnitsConversionFactor() : 0,
                VirtualObjectHeight = isVirtualObject ? 0.4 * UnitsConverter.GetMetersToUnitsConversionFactor() : 0
            };

            int itemId = await manager.AddQueueItemAsync(item).ConfigureAwait(false);

            // 真实物体：主实例解析名字并写入 MovingObject 引用（BatchQueueItems 表无 MovingObjectName 列，
            // F1 副实例检测从 ModelItemReferences 读取移动物体名）
            if (!isVirtualObject)
            {
                if (string.IsNullOrWhiteSpace(movingObjectName))
                {
                    throw new InvalidOperationException("真实物体批处理必须提供 movingObjectName");
                }

                string resolvedDisplayName = null;
                string resolvedPathId = null;
                int resolvedModelIndex = 0;
                InvokeOnUiThread(() =>
                {
                    Document doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
                    foreach (Model model in doc.Models)
                    {
                        if (model?.RootItem == null)
                        {
                            continue;
                        }

                        foreach (ModelItem match in model.RootItem.DescendantsAndSelf)
                        {
                            if (match == null)
                            {
                                continue;
                            }

                            if (string.Equals(match.DisplayName, movingObjectName, StringComparison.OrdinalIgnoreCase))
                            {
                                resolvedDisplayName = match.DisplayName;
                                try
                                {
                                    var path = doc.Models.CreatePathId(match);
                                    resolvedModelIndex = path.ModelIndex;
                                    resolvedPathId = path.PathId;
                                }
                                catch
                                {
                                }
                                return true;
                            }
                        }
                    }

                    return true;
                });

                if (string.IsNullOrWhiteSpace(resolvedDisplayName))
                {
                    throw new InvalidOperationException($"找不到移动物体: {movingObjectName}");
                }

                await pathManager.AddModelItemReferenceAsync(
                    itemId, "BatchQueueItem", resolvedModelIndex, resolvedPathId,
                    resolvedDisplayName, movingObjectName, "MovingObject").ConfigureAwait(false);

                LogManager.Info($"[测试HTTP] 已写入批处理移动物体引用: {movingObjectName} (itemId={itemId})");
            }

            LogManager.Info($"[测试HTTP] 已添加批量队列项: {routeName}, itemId={itemId}, queueCount={manager.QueueCount}");

            return new
            {
                itemId,
                routeName,
                queueCount = manager.QueueCount,
                isExecuting = manager.IsExecuting
            };
        }

        private static object AnalyzePathPayload(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            string routeName = GetRequiredQueryValue(query, "routeName");
            string strategy = GetOptionalQueryValue(query, "strategy") ?? "安全优先";

            PathRoute route = pathManager
                .GetAllRoutes()
                ?.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
            if (route == null)
            {
                throw new InvalidOperationException($"找不到指定路径: {routeName}");
            }

            PathAnalysisResult result = pathManager.AnalyzeRoute(route, strategy);

            return new
            {
                routeName,
                routeId = result.RouteId,
                strategy = result.Strategy,
                collisionCount = result.CollisionCount,
                safetyScore = result.SafetyScore,
                efficiencyScore = result.EfficiencyScore,
                overallScore = result.OverallScore,
                turnDifficultyScore = result.TurnDifficultyScore,
                tortuosityScore = result.TortuosityScore,
                redundancyScore = result.RedundancyScore,
                hotspotCount = result.HotspotCount,
                analyzedAt = result.AnalysisTime.ToString("o")
            };
        }

        private static object BuildDetectionRecordPayload(Dictionary<string, string> query)
        {
            string raw = GetRequiredQueryValue(query, "id");
            if (!int.TryParse(raw, out int id) || id <= 0)
            {
                throw new InvalidOperationException($"无法解析检测记录 ID: {raw}");
            }

            PathPlanningManager pathManager = RequirePathManager();
            CollisionDetectionRecord record = pathManager.GetDetectionRecordById(id);
            if (record == null)
            {
                throw new InvalidOperationException($"检测记录不存在: {id}");
            }

            return new
            {
                id = record.Id,
                routeId = record.RouteId,
                testName = record.TestName,
                frameRate = record.FrameRate,
                durationSeconds = record.DurationSeconds,
                isVirtualObject = record.IsVirtualObject,
                animatedObjectName = record.AnimatedObjectName,
                collisionCount = record.CollisionCount,
                createdAt = record.CreatedTime.ToString("o")
            };
        }

        /// <summary>
        /// 解析实例端口：优先环境变量 TRANSPORTPLUGIN_TEST_PORT，默认 18777。
        /// 支持多 Navisworks 实例并行（E4 副实例检测 worker）。
        /// </summary>
        private int ResolvePort()
        {
            if (_portResolved)
            {
                return _resolvedPort;
            }

            int port = DefaultPort;
            string raw = Environment.GetEnvironmentVariable(PortEnvironmentVariable);
            if (!string.IsNullOrWhiteSpace(raw) && int.TryParse(raw, out int parsedPort) && parsedPort > 0 && parsedPort < 65536)
            {
                port = parsedPort;
            }

            _resolvedPort = port;
            _portResolved = true;
            return port;
        }

        /// <summary>
        /// F2：序列化碰撞对象详情（name + 副实例 PathId + 层级链），
        /// 供主 NW 按层级链（导出保留树结构，去根一致）映射回主模型。
        /// </summary>
        private static List<object> SerializeCollidedObjects(
            List<CollisionResult> allCollisions, List<string> nameList)
        {
            var result = new List<object>();
            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            if (allCollisions != null)
            {
                foreach (var collision in allCollisions)
                {
                    if (collision?.Item2 == null)
                    {
                        continue;
                    }

                    string name = collision.Item2.DisplayName;
                    if (string.IsNullOrWhiteSpace(name) || !seen.Add(name))
                    {
                        continue;
                    }

                    if (result.Count >= 50)
                    {
                        break;
                    }

                    string pathId = null;
                    try
                    {
                        var path = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(collision.Item2);
                        pathId = path.PathId;
                    }
                    catch
                    {
                    }

                    result.Add(new
                    {
                        name,
                        pathId,
                        ancestorChain = BuildAncestorNameChain(collision.Item2)
                    });
                }
            }

            // fallback：AllCollisions 无 Item2 引用时退回名字列表
            if (result.Count == 0 && nameList != null)
            {
                foreach (string name in nameList.Take(50))
                {
                    if (seen.Add(name))
                    {
                        result.Add(new { name, pathId = (string)null, ancestorChain = (string)null });
                    }
                }
            }

            return result;
        }

        private object BuildSelectionPayload()
        {
            Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
            var selectedItems = activeDocument?.CurrentSelection?.SelectedItems?.Cast<ModelItem>().Where(item => item != null).ToList()
                               ?? new List<ModelItem>();

            return new
            {
                hasActiveDocument = activeDocument != null,
                selectionCount = selectedItems.Count,
                selectedItems = selectedItems.Select(SerializeModelItem).ToList()
            };
        }

        private object SelectRoutePayload(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            string routeName = GetRequiredQueryValue(query, "name");
            List<PathRoute> routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();

            PathRoute selectedRoute = routes.FirstOrDefault(route =>
                string.Equals(route.Name, routeName, StringComparison.OrdinalIgnoreCase));
            if (selectedRoute == null)
            {
                throw new InvalidOperationException($"找不到指定路径: {routeName}");
            }

            pathManager.SetCurrentRoute(selectedRoute);
            LogManager.Info($"[测试HTTP] 已切换当前路径: {selectedRoute.Name} ({selectedRoute.PathType})");

            return new
            {
                selected = SerializeRoute(selectedRoute, selectedRoute),
                totalRouteCount = routes.Count
            };
        }

        private object SelectDefaultRoutePayload(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            string pathTypeValue = GetRequiredQueryValue(query, "pathType");
            string prefix = GetOptionalQueryValue(query, "prefix") ?? DefaultAutoTestRoutePrefix;

            if (!Enum.TryParse(pathTypeValue, true, out PathType pathType))
            {
                throw new InvalidOperationException($"无法解析路径类型: {pathTypeValue}");
            }

            List<PathRoute> routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
            PathRoute selectedRoute = routes
                .FirstOrDefault(route =>
                    route.PathType == pathType &&
                    !string.IsNullOrWhiteSpace(route.Name) &&
                    route.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));

            if (selectedRoute == null &&
                string.Equals(prefix, DefaultAutoTestRoutePrefix, StringComparison.Ordinal))
            {
                // 标准自动测试路径缺失时自动导入后重试（仅限自动测试_ 前缀）
                EnsureAutoTestRoutesImported(pathManager);

                routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
                selectedRoute = routes
                    .FirstOrDefault(route =>
                        route.PathType == pathType &&
                        !string.IsNullOrWhiteSpace(route.Name) &&
                        route.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
            }

            if (selectedRoute == null)
            {
                throw new InvalidOperationException(
                    $"找不到默认测试路径: prefix={prefix}, pathType={pathType}");
            }

            pathManager.SetCurrentRoute(selectedRoute);
            LogManager.Info($"[测试HTTP] 已切换默认测试路径: {selectedRoute.Name} ({selectedRoute.PathType})");

            return new
            {
                selectionRule = new
                {
                    prefix,
                    pathType = pathType.ToString()
                },
                selected = SerializeRoute(selectedRoute, selectedRoute)
            };
        }

        private object SelectAnimatedObjectPayload(Dictionary<string, string> query)
        {
            AnimationControlViewModel animationViewModel = RequireAnimationControlViewModel();
            PathPlanningManager pathManager = RequirePathManager();
            animationViewModel.SetPathPlanningManager(pathManager);

            ModelItem animatedObject = ResolveAnimatedObjectFromQuery(query);
            SelectDocumentItem(animatedObject);

            if (!animationViewModel.SelectAnimatedObjectCommand.CanExecute(null))
            {
                throw new InvalidOperationException("当前动画视图模型不允许执行“选择移动物体”命令");
            }

            animationViewModel.UseVirtualObject = false;
            animationViewModel.SelectAnimatedObjectCommand.Execute(null);

            if (!IsSameModelItem(animationViewModel.SelectedAnimatedObject, animatedObject))
            {
                throw new InvalidOperationException("选择真实物体失败：动画视图模型未保留选中的对象");
            }

            return new
            {
                selectedAnimatedObject = SerializeModelItem(animatedObject),
                canGenerateAnimation = animationViewModel.CanGenerateAnimation,
                isManualCollisionTargetEnabled = animationViewModel.IsManualCollisionTargetEnabled
            };
        }

        private async Task<object> RunVirtualCollisionTestAsync(Dictionary<string, string> query, PathType? fixedPathType)
        {
            await _testExecutionLock.WaitAsync().ConfigureAwait(false);
            try
            {
                return await RunVirtualCollisionTestCoreAsync(query, fixedPathType).ConfigureAwait(false);
            }
            finally
            {
                _testExecutionLock.Release();
            }
        }

        private async Task<object> RunVirtualCollisionTestCoreAsync(Dictionary<string, string> query, PathType? fixedPathType)
        {
            using (EnableAutoConfirmCollisionAnalysisDialogs())
            using (EnableAutoChooseCreateNewDetectionRecord())
            {
                int timeoutSeconds = ParseTimeoutSeconds(query, 180);
                DateTime deadlineUtc = DateTime.UtcNow.AddSeconds(timeoutSeconds);

                var setupResult = InvokeOnUiThread(() => PrepareVirtualCollisionTest(query, fixedPathType));

                await WaitForConditionAsync(
                    deadlineUtc,
                    () => InvokeOnUiThread(() =>
                    {
                        var animationManager = PathAnimationManager.GetInstance();
                        return animationManager != null &&
                               animationManager.TotalFrames > 0 &&
                               (animationManager.CurrentState == AnimationState.Ready ||
                                animationManager.CurrentState == AnimationState.Finished);
                    }),
                    "等待动画生成完成超时").ConfigureAwait(false);

                InvokeOnUiThread(() =>
                {
                    StartPreparedAnimation();
                    return true;
                });

                await WaitForConditionAsync(
                    deadlineUtc,
                    () => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentState == AnimationState.Finished),
                    "等待动画播放和碰撞检测完成超时").ConfigureAwait(false);

                await WaitForConditionAsync(
                    deadlineUtc,
                    () => InvokeOnUiThread(() =>
                    {
                        var animationVm = RequireAnimationControlViewModel();
                        return animationVm.HasGeneratedCollisionReport;
                    }),
                    "等待碰撞报告生成超时").ConfigureAwait(false);

                return InvokeOnUiThread(() =>
                {
                    AnimationControlViewModel animationVm = RequireAnimationControlViewModel();
                    PathAnimationManager animationManager = PathAnimationManager.GetInstance();
                    CollisionReportResult report = animationVm.LastGeneratedReport;

                    return new
                    {
                        route = setupResult.route,
                        animatedObject = setupResult.animatedObject,
                        requestedPathType = setupResult.pathType.ToString(),
                        timeoutSeconds,
                        automation = new
                        {
                            autoConfirmedCollisionAnalysisDialog = true,
                            autoChooseCreateNewDetectionRecord = true
                        },
                        animation = new
                        {
                            currentState = animationManager?.CurrentState.ToString(),
                            totalFrames = animationManager?.TotalFrames ?? 0,
                            currentFrame = animationManager?.CurrentFrame ?? 0,
                            detectionRecordId = animationManager?.CurrentDetectionRecordId
                        },
                        report = report == null
                            ? null
                            : new
                            {
                                totalCollisions = report.TotalCollisions,
                                uniqueCollidedObjectsCount = report.UniqueCollidedObjectsCount,
                                pathName = report.PathName,
                                movingObjectInfo = report.MovingObjectInfo,
                                hasScreenshots = report.Screenshots != null && report.Screenshots.Count > 0,
                                screenshotCount = report.Screenshots?.Count ?? 0,
                                resultId = report.ResultId,
                                routeId = report.RouteId,
                                // F2：碰撞对象详情（name + 副实例 PathId + 层级链），用于跨实例报告映射回主模型
                                collidedObjects = SerializeCollidedObjects(report.AllCollisions, report.CollidedObjectsList)
                            }
                    };
                });
            }
        }

        private static object BuildEnvelope(bool ok, object data = null, string error = null)
        {
            return new Dictionary<string, object>
            {
                ["ok"] = ok,
                ["data"] = data,
                ["error"] = error
            };
        }

        private static T InvokeOnUiThread<T>(Func<T> func)
        {
            var dispatcher = System.Windows.Application.Current?.Dispatcher;
            if (dispatcher == null || dispatcher.CheckAccess())
            {
                return func();
            }

            return dispatcher.Invoke(func);
        }

        private static ModelItem ResolveControlledObject(PathAnimationManager animationManager)
        {
            if (VirtualObjectManager.Instance.IsVirtualObjectActive &&
                VirtualObjectManager.Instance.CurrentVirtualObject != null)
            {
                return VirtualObjectManager.Instance.CurrentVirtualObject;
            }

            return animationManager?.AnimatedObject;
        }

        private static object SerializeModelItem(ModelItem item)
        {
            if (item == null)
            {
                return null;
            }

            return new
            {
                displayName = item.DisplayName,
                instanceGuid = item.InstanceGuid.ToString()
            };
        }

        private static object SerializeRoute(PathRoute route, PathRoute currentRoute)
        {
            return new
            {
                id = route?.Id,
                name = route?.Name,
                pathType = route?.PathType.ToString(),
                pointCount = route?.Points?.Count ?? 0,
                isCurrent = currentRoute != null && ReferenceEquals(route, currentRoute),
                matchesAutoTestPrefix = !string.IsNullOrWhiteSpace(route?.Name) &&
                                        route.Name.StartsWith(DefaultAutoTestRoutePrefix, StringComparison.OrdinalIgnoreCase)
            };
        }

        private static PathRoute TryFindAutoTestRoute(
            IEnumerable<PathRoute> routes,
            PathType pathType,
            string routePrefix = DefaultAutoTestRoutePrefix)
        {
            string effectivePrefix = string.IsNullOrWhiteSpace(routePrefix)
                ? DefaultAutoTestRoutePrefix
                : routePrefix;

            return routes?.FirstOrDefault(route =>
                route != null &&
                route.PathType == pathType &&
                !string.IsNullOrWhiteSpace(route.Name) &&
                route.Name.StartsWith(effectivePrefix, StringComparison.OrdinalIgnoreCase));
        }

        private static object AnalyzeAutoPathGridPayload(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, null);
            if (route.Points == null || route.Points.Count < 2)
            {
                throw new InvalidOperationException($"路径点不足，无法分析自动路径网格: {route.Name}");
            }

            List<PathPoint> orderedPoints = route.Points.OrderBy(point => point.Index).ToList();
            PathPoint startPoint = orderedPoints.First();
            PathPoint endPoint = orderedPoints.Last();

            BoundingBox3D bounds = InvokePathManagerPrivate<BoundingBox3D>(pathManager, "GetModelBounds");
            double gridSize = InvokePathManagerPrivate<double>(pathManager, "CalculateOptimalGridSize", bounds);

            var config = ConfigManager.Instance.Current?.PathEditing
                         ?? throw new InvalidOperationException("缺少 PathEditing 配置，无法分析自动路径网格");

            double objectLengthInMeters = config.ObjectLengthMeters;
            double objectWidthInMeters = config.ObjectWidthMeters;
            double objectHeightInMeters = config.ObjectHeightMeters;
            double safetyMarginInMeters = config.SafetyMarginMeters;
            double objectRadiusInMeters = Math.Max(objectLengthInMeters, objectWidthInMeters) / 2.0;

            var gridMapGenerator = new GridMapGenerator();
            GridMap gridMap = gridMapGenerator.GenerateFromBIM(
                bounds,
                gridSize,
                objectRadiusInMeters,
                safetyMarginInMeters,
                startPoint.Position,
                endPoint.Position,
                objectHeightInMeters);

            return new
            {
                requestedPathType = route.PathType.ToString(),
                route = new
                {
                    id = route.Id,
                    name = route.Name,
                    pathType = route.PathType.ToString(),
                    pointCount = route.Points.Count,
                    startPoint = SerializePathPoint(startPoint),
                    endPoint = SerializePathPoint(endPoint)
                },
                planningParameters = new
                {
                    gridSizeInMeters = gridSize,
                    objectLengthInMeters,
                    objectWidthInMeters,
                    objectHeightInMeters,
                    objectRadiusInMeters,
                    safetyMarginInMeters
                },
                bounds = new
                {
                    min = SerializePoint3D(bounds.Min),
                    max = SerializePoint3D(bounds.Max)
                },
                gridStats = SerializeGridStats(gridMap),
                obstacleDiagnostics = BuildObstacleDiagnostics(gridMapGenerator, gridMap, objectHeightInMeters, safetyMarginInMeters)
            };
        }

        private static object RunAutoPathPayload(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, null);
            if (route.Points == null || route.Points.Count < 2)
            {
                throw new InvalidOperationException($"路径点不足，无法执行自动路径规划: {route.Name}");
            }

            List<PathPoint> orderedPoints = route.Points.OrderBy(point => point.Index).ToList();
            PathPoint startPoint = orderedPoints.First();
            PathPoint endPoint = orderedPoints.Last();

            var config = ConfigManager.Instance.Current?.PathEditing
                         ?? throw new InvalidOperationException("缺少 PathEditing 配置，无法执行自动路径规划");

            BoundingBox3D bounds = InvokePathManagerPrivate<BoundingBox3D>(pathManager, "GetModelBounds");
            double gridSize = ParsePositiveDoubleQuery(
                query,
                "gridSizeInMeters",
                InvokePathManagerPrivate<double>(pathManager, "CalculateOptimalGridSize", bounds));
            double objectHeightInMeters = config.ObjectHeightMeters;
            double objectLengthInMeters = ParsePositiveDoubleQuery(query, "objectLengthInMeters", config.ObjectLengthMeters);
            double objectWidthInMeters = ParsePositiveDoubleQuery(query, "objectWidthInMeters", config.ObjectWidthMeters);
            double safetyMarginInMeters = ParseNonNegativeDoubleQuery(query, "safetyMarginInMeters", config.SafetyMarginMeters);
            double objectRadiusInMeters = Math.Max(objectLengthInMeters, objectWidthInMeters) / 2.0;
            PathStrategy strategy = PathStrategy.Shortest;

            string strategyText = GetOptionalQueryValue(query, "strategy");
            if (!string.IsNullOrWhiteSpace(strategyText) &&
                Enum.TryParse(strategyText, true, out PathStrategy parsedStrategy))
            {
                strategy = parsedStrategy;
            }

            PathRoute autoRoute = pathManager
                .AutoPlanPath(
                    startPoint,
                    endPoint,
                    objectRadiusInMeters,
                    safetyMarginInMeters,
                    gridSize,
                    objectHeightInMeters,
                    strategy)
                .GetAwaiter()
                .GetResult();

            if (autoRoute == null)
            {
                throw new InvalidOperationException("AutoPlanPath 返回空结果");
            }

            return new
            {
                sourceRoute = new
                {
                    id = route.Id,
                    name = route.Name,
                    pathType = route.PathType.ToString()
                },
                generatedRoute = new
                {
                    id = autoRoute.Id,
                    name = autoRoute.Name,
                    pathType = autoRoute.PathType.ToString(),
                    pointCount = autoRoute.Points?.Count ?? 0,
                    length = autoRoute.TotalLength
                },
                planningParameters = new
                {
                    gridSizeInMeters = gridSize,
                    objectHeightInMeters,
                    objectLengthInMeters,
                    objectWidthInMeters,
                    objectRadiusInMeters,
                    safetyMarginInMeters,
                    strategy = strategy.ToString()
                },
                segmentValidation = ValidateRouteSegmentsAgainstGridMap(autoRoute, autoRoute.AssociatedGridMap)
            };
        }

        /// <summary>
        /// 导入路径文件（JSON/XML）并注册到路径管理器，返回导入路径的完整信息。
        /// 供集成测试验证“导入 → 属性映射 → 长度计算 → 路径可用性”完整流程。
        /// overwrite=true 时同名路径先删除后导入（保证测试可重复执行且不累积数据）。
        /// </summary>
        /// <summary>
        /// 导出路径到文件（XML/JSON），供集成测试验证导出→导入回环一致性。
        /// query：routeName（必填）、format（xml/json，默认 xml）、path（可选导出路径）。
        /// </summary>
        private static object ExportRouteFilePayload(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            string routeName = GetRequiredQueryValue(query, "routeName");
            string formatText = GetOptionalQueryValue(query, "format") ?? "xml";

            PathRoute route = pathManager
                .GetAllRoutes()
                ?.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
            if (route == null)
            {
                throw new InvalidOperationException($"找不到指定路径: {routeName}");
            }

            bool isJson = string.Equals(formatText, "json", StringComparison.OrdinalIgnoreCase);
            string extension = isJson ? ".json" : ".xml";

            string exportPath = GetOptionalQueryValue(query, "path");
            if (string.IsNullOrWhiteSpace(exportPath))
            {
                exportPath = Path.Combine(Path.GetTempPath(), $"route-export-{route.Id}-{Guid.NewGuid():N}{extension}");
            }

            string directory = Path.GetDirectoryName(exportPath);
            if (!string.IsNullOrWhiteSpace(directory))
            {
                Directory.CreateDirectory(directory);
            }

            var dataManager = new PathDataManager();
            bool exported = isJson
                ? dataManager.ExportToJson(new List<PathRoute> { route }, exportPath)
                : dataManager.ExportToXml(new List<PathRoute> { route }, exportPath);

            if (!exported)
            {
                throw new InvalidOperationException($"导出路径失败: {routeName} -> {exportPath}");
            }

            LogManager.Info($"[测试HTTP] 已导出路径: {routeName} ({formatText}) -> {exportPath}");

            return new
            {
                routeName,
                format = isJson ? "json" : "xml",
                exportedFilePath = exportPath,
                pointCount = route.Points?.Count ?? 0,
                totalLengthInMeters = route.TotalLength
            };
        }

        private static object ImportRouteFilePayload(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            string filePath = GetRequiredQueryValue(query, "path");
            string formatText = GetOptionalQueryValue(query, "format");
            bool overwrite = ParseBooleanQuery(query, "overwrite", false);

            if (!File.Exists(filePath))
            {
                throw new InvalidOperationException($"导入文件不存在: {filePath}");
            }

            ExportFormat format;
            if (string.IsNullOrWhiteSpace(formatText))
            {
                string extension = Path.GetExtension(filePath).ToLowerInvariant();
                format = string.Equals(extension, ".json", StringComparison.Ordinal)
                    ? ExportFormat.Json
                    : ExportFormat.Xml;
            }
            else if (Enum.TryParse(formatText, true, out ExportFormat parsedFormat) &&
                     (parsedFormat == ExportFormat.Json || parsedFormat == ExportFormat.Xml))
            {
                format = parsedFormat;
            }
            else
            {
                throw new InvalidOperationException($"无法解析导入格式: {formatText}（仅支持 json/xml）");
            }

            var dataManager = new PathDataManager();
            List<PathRoute> importedRoutes = format == ExportFormat.Json
                ? dataManager.ImportFromJson(filePath)
                : dataManager.ImportFromXml(filePath);

            if (importedRoutes == null || importedRoutes.Count == 0)
            {
                throw new InvalidOperationException($"导入文件中没有有效路径: {filePath}");
            }

            var serializedRoutes = new List<object>();
            foreach (PathRoute importedRoute in importedRoutes)
            {
                if (overwrite)
                {
                    // 覆盖模式：先删除同名路径，保证导入名称精确、测试可重复执行
                    PathRoute existingRoute = pathManager
                        .GetAllRoutes()
                        ?.FirstOrDefault(r => string.Equals(r.Name, importedRoute.Name, StringComparison.Ordinal));
                    if (existingRoute != null)
                    {
                        pathManager.DeleteRoute(existingRoute);
                        LogManager.Info($"[测试HTTP] 已删除同名路径以便覆盖导入: {existingRoute.Name}");
                    }
                }

                // 注册到路径管理器（自动入库），供后续切换/动画流程使用
                pathManager.AddRoute(importedRoute);

                serializedRoutes.Add(new
                {
                    id = importedRoute.Id,
                    name = importedRoute.Name,
                    pathType = importedRoute.PathType.ToString(),
                    pointCount = importedRoute.Points?.Count ?? 0,
                    totalLengthInMeters = importedRoute.TotalLength,
                    railMountMode = importedRoute.RailMountMode.ToString(),
                    railPathDefinitionMode = importedRoute.RailPathDefinitionMode.ToString(),
                    railNormalOffset = importedRoute.RailNormalOffset,
                    railPreferredNormal = importedRoute.RailPreferredNormal == null
                        ? null
                        : new
                        {
                            x = importedRoute.RailPreferredNormal.X,
                            y = importedRoute.RailPreferredNormal.Y,
                            z = importedRoute.RailPreferredNormal.Z
                        }
                });
            }

            LogManager.Info($"[测试HTTP] 已导入路径文件 {Path.GetFileName(filePath)}，共 {serializedRoutes.Count} 条");

            Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
            double metersToModelUnits = activeDocument == null
                ? 1.0
                : UnitsConverter.GetMetersToUnitsConversionFactor(activeDocument.Units);

            return new
            {
                filePath,
                format = format.ToString(),
                importedCount = serializedRoutes.Count,
                metersToModelUnits,
                routes = serializedRoutes
            };
        }

        private static object ValidateRouteSegmentsAgainstGridMap(PathRoute route, GridMap gridMap)
        {
            if (route?.Points == null || route.Points.Count < 2 || gridMap == null)
            {
                return new
                {
                    isAvailable = false,
                    segmentCount = 0,
                    sampleCount = 0,
                    invalidSampleCount = 0,
                    blockedSampleCount = 0,
                    failures = new object[0]
                };
            }

            var orderedPoints = route.Points.OrderBy(point => point.Index).ToList();
            var failures = new List<object>();
            int sampleCount = 0;
            int invalidSampleCount = 0;
            int blockedSampleCount = 0;
            int blockedCellInteriorIntersectionCount = 0;
            double sampleSpacing = Math.Max(gridMap.CellSize * 0.25, 1e-6);

            for (int segmentIndex = 0; segmentIndex < orderedPoints.Count - 1; segmentIndex++)
            {
                Point3D start = orderedPoints[segmentIndex].Position;
                Point3D end = orderedPoints[segmentIndex + 1].Position;
                double segmentLength = CalculateHostHorizontalDistance(start, end, gridMap.CoordinateSystemType);
                int samples = Math.Max(1, (int)Math.Ceiling(segmentLength / sampleSpacing));

                for (int sampleIndex = 0; sampleIndex <= samples; sampleIndex++)
                {
                    double t = samples == 0 ? 0.0 : (double)sampleIndex / samples;
                    Point3D samplePoint = InterpolateHostPoint(start, end, t);
                    double sampleElevation = gridMap.GetWorldElevation(samplePoint);
                    GridPoint2D gridPosition = gridMap.WorldToGrid(samplePoint);
                    sampleCount++;

                    if (!gridMap.IsValidGridPosition(gridPosition))
                    {
                        invalidSampleCount++;
                        AddFailure(failures, segmentIndex, sampleIndex, samplePoint, gridPosition, "GridOutOfRange");
                        continue;
                    }

                    if (!gridMap.IsPassableAtElevation(gridPosition, sampleElevation, 0.1))
                    {
                        blockedSampleCount++;
                        AddFailure(failures, segmentIndex, sampleIndex, samplePoint, gridPosition, "NotPassableAtElevation");
                    }
                }

                blockedCellInteriorIntersectionCount += CountBlockedCellInteriorIntersections(
                    start,
                    end,
                    gridMap,
                    segmentIndex,
                    failures);
            }

            return new
            {
                isAvailable = true,
                segmentCount = Math.Max(0, orderedPoints.Count - 1),
                sampleCount,
                invalidSampleCount,
                blockedSampleCount,
                blockedCellInteriorIntersectionCount,
                failures = failures.Take(20).ToList()
            };
        }

        private static object BuildRouteGridDiagnosticsPayload(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            string routeName = GetOptionalQueryValue(query, "routeName");
            List<PathRoute> routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();

            PathRoute route = string.IsNullOrWhiteSpace(routeName)
                ? pathManager.CurrentRoute
                : routes.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));

            if (route == null)
            {
                throw new InvalidOperationException(
                    string.IsNullOrWhiteSpace(routeName)
                        ? "当前没有选中的路径"
                        : $"找不到指定路径: {routeName}");
            }

            GridMap gridMap = route.AssociatedGridMap;
            if (gridMap == null)
            {
                if (!pathManager.TryRestoreGridMapForRoute(route, out gridMap, out string restoreMessage))
                {
                    throw new InvalidOperationException($"路径没有关联 GridMap，且无法按保存参数重建，无法诊断: {route.Name}。{restoreMessage}");
                }
            }

            var orderedPoints = route.Points?.OrderBy(point => point.Index).ToList() ?? new List<PathPoint>();
            var segmentDiagnostics = new List<object>();
            var blockedIntersections = new List<object>();
            int intersectedCellCount = 0;

            for (int segmentIndex = 0; segmentIndex < orderedPoints.Count - 1; segmentIndex++)
            {
                PathPoint startPathPoint = orderedPoints[segmentIndex];
                PathPoint endPathPoint = orderedPoints[segmentIndex + 1];
                Point3D start = startPathPoint.Position;
                Point3D end = endPathPoint.Position;
                var intersectedCells = GetSegmentIntersectedCells(start, end, gridMap);
                intersectedCellCount += intersectedCells.Count;

                var blockedCells = intersectedCells
                    .Where(cell => !cell.isPassableAtSegmentElevation)
                    .ToList();

                foreach (var blockedCell in blockedCells)
                {
                    if (blockedIntersections.Count < 100)
                    {
                        blockedIntersections.Add(new
                        {
                            segmentIndex,
                            startPointIndex = startPathPoint.Index,
                            endPointIndex = endPathPoint.Index,
                            cell = blockedCell
                        });
                    }
                }

                segmentDiagnostics.Add(new
                {
                    segmentIndex,
                    startPointIndex = startPathPoint.Index,
                    endPointIndex = endPathPoint.Index,
                    startGrid = SerializeGridPosition(gridMap.WorldToGrid(start)),
                    endGrid = SerializeGridPosition(gridMap.WorldToGrid(end)),
                    startHorizontal = SerializeHorizontalPoint(start, gridMap),
                    endHorizontal = SerializeHorizontalPoint(end, gridMap),
                    intersectedCellCount = intersectedCells.Count,
                    blockedCellCount = blockedCells.Count,
                    intersectedCells = intersectedCells.Take(200).ToList()
                });
            }

            return new
            {
                route = new
                {
                    id = route.Id,
                    name = route.Name,
                    pathType = route.PathType.ToString(),
                    pointCount = orderedPoints.Count,
                    length = route.TotalLength
                },
                grid = new
                {
                    coordinateSystemType = gridMap.CoordinateSystemType.ToString(),
                    width = gridMap.Width,
                    height = gridMap.Height,
                    cellSize = gridMap.CellSize,
                    cellSizeInMeters = UnitsConverter.ConvertToMeters(gridMap.CellSize),
                    origin = SerializePoint3D(gridMap.Origin),
                    originHorizontal = SerializeHorizontalPoint(gridMap.Origin, gridMap)
                },
                points = orderedPoints.Select(point => SerializePathPointWithGrid(point, gridMap)).ToList(),
                segmentCount = Math.Max(0, orderedPoints.Count - 1),
                intersectedCellCount,
                blockedIntersectionCount = blockedIntersections.Count,
                blockedIntersections,
                segments = segmentDiagnostics
            };
        }

        private static double CalculateHostHorizontalDistance(Point3D start, Point3D end, CoordinateSystemType coordinateSystemType)
        {
            var adapter = new HostCoordinateAdapter(coordinateSystemType);
            var startHorizontal = HostPlanarGridHelper.GetHorizontalCoords3(ToVector3(start), adapter);
            var endHorizontal = HostPlanarGridHelper.GetHorizontalCoords3(ToVector3(end), adapter);
            double dh1 = endHorizontal.h1 - startHorizontal.h1;
            double dh2 = endHorizontal.h2 - startHorizontal.h2;
            return Math.Sqrt(dh1 * dh1 + dh2 * dh2);
        }

        private static Point3D InterpolateHostPoint(Point3D start, Point3D end, double t)
        {
            return new Point3D(
                start.X + t * (end.X - start.X),
                start.Y + t * (end.Y - start.Y),
                start.Z + t * (end.Z - start.Z));
        }

        private static System.Numerics.Vector3 ToVector3(Point3D point)
        {
            return new System.Numerics.Vector3((float)point.X, (float)point.Y, (float)point.Z);
        }

        private static int CountBlockedCellInteriorIntersections(
            Point3D start,
            Point3D end,
            GridMap gridMap,
            int segmentIndex,
            List<object> failures)
        {
            var startHorizontal = GetHostHorizontalCoords(start, gridMap);
            var endHorizontal = GetHostHorizontalCoords(end, gridMap);
            var originHorizontal = GetHostHorizontalCoords(gridMap.Origin, gridMap);

            int minGridX = (int)Math.Round((Math.Min(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) - 1;
            int maxGridX = (int)Math.Round((Math.Max(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) + 1;
            int minGridY = (int)Math.Round((Math.Min(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) - 1;
            int maxGridY = (int)Math.Round((Math.Max(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) + 1;

            minGridX = Math.Max(0, minGridX);
            minGridY = Math.Max(0, minGridY);
            maxGridX = Math.Min(gridMap.Width - 1, maxGridX);
            maxGridY = Math.Min(gridMap.Height - 1, maxGridY);

            int blockedIntersections = 0;

            for (int x = minGridX; x <= maxGridX; x++)
            {
                for (int y = minGridY; y <= maxGridY; y++)
                {
                    var gridPosition = new GridPoint2D(x, y);

                    if (!gridMap.TryGetSegmentGridCellInteriorIntersection(start, end, gridPosition, out double enterT, out double exitT))
                    {
                        continue;
                    }

                    double sampleT = Math.Max(0.0, Math.Min(1.0, (enterT + exitT) * 0.5));
                    Point3D samplePoint = InterpolateHostPoint(start, end, sampleT);
                    double sampleElevation = gridMap.GetWorldElevation(samplePoint);

                    if (!gridMap.IsPassableAtElevation(gridPosition, sampleElevation, 0.1))
                    {
                        blockedIntersections++;
                        AddFailure(
                            failures,
                            segmentIndex,
                            -1,
                            samplePoint,
                            gridPosition,
                            $"BlockedCellInteriorIntersection:t=[{enterT:F6},{exitT:F6}]");
                    }
                }
            }

            return blockedIntersections;
        }

        private static List<dynamic> GetSegmentIntersectedCells(Point3D start, Point3D end, GridMap gridMap)
        {
            var startHorizontal = GetHostHorizontalCoords(start, gridMap);
            var endHorizontal = GetHostHorizontalCoords(end, gridMap);
            var originHorizontal = GetHostHorizontalCoords(gridMap.Origin, gridMap);

            int minGridX = (int)Math.Round((Math.Min(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) - 1;
            int maxGridX = (int)Math.Round((Math.Max(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) + 1;
            int minGridY = (int)Math.Round((Math.Min(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) - 1;
            int maxGridY = (int)Math.Round((Math.Max(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) + 1;

            minGridX = Math.Max(0, minGridX);
            minGridY = Math.Max(0, minGridY);
            maxGridX = Math.Min(gridMap.Width - 1, maxGridX);
            maxGridY = Math.Min(gridMap.Height - 1, maxGridY);

            var result = new List<dynamic>();

            for (int x = minGridX; x <= maxGridX; x++)
            {
                for (int y = minGridY; y <= maxGridY; y++)
                {
                    var gridPosition = new GridPoint2D(x, y);

                    if (!gridMap.TryGetSegmentGridCellInteriorIntersection(start, end, gridPosition, out double enterT, out double exitT))
                    {
                        continue;
                    }

                    double sampleT = Math.Max(0.0, Math.Min(1.0, (enterT + exitT) * 0.5));
                    Point3D samplePoint = InterpolateHostPoint(start, end, sampleT);
                    double sampleElevation = gridMap.GetWorldElevation(samplePoint);
                    GridCell cell = gridMap.Cells[x, y];
                    var bounds = gridMap.GetGridCellPlanarBounds(gridPosition);
                    bool isPassableAtSegmentElevation = gridMap.IsPassableAtElevation(gridPosition, sampleElevation, 0.1);

                    result.Add(new
                    {
                        grid = SerializeGridPosition(gridPosition),
                        cellType = cell.CellType,
                        hasAnyWalkableLayer = cell.HasAnyWalkableLayer(),
                        isPassableAtSegmentElevation,
                        sampleT,
                        enterT,
                        exitT,
                        sampleElevation,
                        samplePoint = SerializePoint3D(samplePoint),
                        center = SerializePoint3D(gridMap.GridToWorld2D(gridPosition)),
                        planarBounds = new
                        {
                            bounds.minH1,
                            bounds.maxH1,
                            bounds.minH2,
                            bounds.maxH2
                        },
                        heightLayers = SerializeHeightLayers(cell)
                    });
                }
            }

            return result
                .OrderBy(cell => cell.grid.x)
                .ThenBy(cell => cell.grid.y)
                .ToList();
        }

        private static (double h1, double h2) GetHostHorizontalCoords(Point3D point, GridMap gridMap)
        {
            var adapter = new HostCoordinateAdapter(gridMap.CoordinateSystemType);
            return HostPlanarGridHelper.GetHorizontalCoords3(ToVector3(point), adapter);
        }

        private static object SerializeGridPosition(GridPoint2D gridPosition)
        {
            return new
            {
                x = gridPosition.X,
                y = gridPosition.Y
            };
        }

        private static object SerializeHorizontalPoint(Point3D point, GridMap gridMap)
        {
            var horizontal = GetHostHorizontalCoords(point, gridMap);
            return new
            {
                h1 = horizontal.h1,
                h2 = horizontal.h2,
                elevation = gridMap.GetWorldElevation(point)
            };
        }

        private static object SerializePathPointWithGrid(PathPoint point, GridMap gridMap)
        {
            GridPoint2D gridPosition = gridMap.WorldToGrid(point.Position);
            return new
            {
                id = point.Id,
                name = point.Name,
                type = point.Type.ToString(),
                index = point.Index,
                position = SerializePoint3D(point.Position),
                horizontal = SerializeHorizontalPoint(point.Position, gridMap),
                grid = SerializeGridPosition(gridPosition),
                cell = SerializeGridCell(gridMap, gridPosition, point.Position)
            };
        }

        private static object SerializeGridCell(GridMap gridMap, GridPoint2D gridPosition, Point3D referencePoint)
        {
            if (!gridMap.IsValidGridPosition(gridPosition))
            {
                return new
                {
                    isValid = false
                };
            }

            GridCell cell = gridMap.Cells[gridPosition.X, gridPosition.Y];
            var bounds = gridMap.GetGridCellPlanarBounds(gridPosition);
            double elevation = gridMap.GetWorldElevation(referencePoint);
            return new
            {
                isValid = true,
                cellType = cell.CellType,
                hasAnyWalkableLayer = cell.HasAnyWalkableLayer(),
                isPassableAtReferenceElevation = gridMap.IsPassableAtElevation(gridPosition, elevation, 0.1),
                planarBounds = new
                {
                    bounds.minH1,
                    bounds.maxH1,
                    bounds.minH2,
                    bounds.maxH2
                },
                heightLayers = SerializeHeightLayers(cell)
            };
        }

        private static object SerializeHeightLayers(GridCell cell)
        {
            return cell.HeightLayers?
                .Select(layer => new
                {
                    z = layer.Z,
                    passableMin = layer.PassableHeight.MinZ,
                    passableMax = layer.PassableHeight.MaxZ,
                    isWalkable = layer.IsWalkable,
                    isBoundary = layer.IsBoundary,
                    type = layer.Type
                })
                .ToList();
        }

        private static void AddFailure(
            List<object> failures,
            int segmentIndex,
            int sampleIndex,
            Point3D samplePoint,
            GridPoint2D gridPosition,
            string reason)
        {
            if (failures.Count >= 20)
            {
                return;
            }

            failures.Add(new
            {
                segmentIndex,
                sampleIndex,
                reason,
                point = SerializePoint3D(samplePoint),
                grid = new
                {
                    x = gridPosition.X,
                    y = gridPosition.Y
                }
            });
        }

        private static PathPlanningManager RequirePathManager()
        {
            PathPlanningManager pathManager = PathPlanningManager.GetActivePathManager();
            if (pathManager == null)
            {
                throw new InvalidOperationException("PathPlanningManager 当前不可用");
            }

            return pathManager;
        }

        private static string GetRequiredQueryValue(Dictionary<string, string> query, string key)
        {
            string value = GetOptionalQueryValue(query, key);
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new InvalidOperationException($"缺少必填参数: {key}");
            }

            return value;
        }

        private static string GetOptionalQueryValue(Dictionary<string, string> query, string key)
        {
            if (query == null || string.IsNullOrWhiteSpace(key))
            {
                return null;
            }

            query.TryGetValue(key, out string value);
            return string.IsNullOrWhiteSpace(value) ? null : value;
        }

        private static double ParsePositiveDoubleQuery(Dictionary<string, string> query, string key, double defaultValue)
        {
            double value = ParseDoubleQuery(query, key, defaultValue);
            if (value <= 0)
            {
                throw new InvalidOperationException($"参数 {key} 必须大于 0，当前值: {value}");
            }

            return value;
        }

        private static double ParseNonNegativeDoubleQuery(Dictionary<string, string> query, string key, double defaultValue)
        {
            double value = ParseDoubleQuery(query, key, defaultValue);
            if (value < 0)
            {
                throw new InvalidOperationException($"参数 {key} 不能小于 0，当前值: {value}");
            }

            return value;
        }

        private static double ParseDoubleQuery(Dictionary<string, string> query, string key, double defaultValue)
        {
            string raw = GetOptionalQueryValue(query, key);
            if (string.IsNullOrWhiteSpace(raw))
            {
                return defaultValue;
            }

            if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out double value))
            {
                throw new InvalidOperationException($"无法解析数值参数 {key}: {raw}");
            }

            return value;
        }

        private static RequestTarget ParseRequestTarget(string requestTarget)
        {
            string path = requestTarget ?? "/";
            var query = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

            int queryStartIndex = path.IndexOf('?');
            if (queryStartIndex >= 0)
            {
                string queryString = path.Substring(queryStartIndex + 1);
                path = path.Substring(0, queryStartIndex);

                foreach (string pair in queryString.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
                {
                    string[] kv = pair.Split(new[] { '=' }, 2);
                    string key = Uri.UnescapeDataString(kv[0] ?? string.Empty);
                    string value = kv.Length > 1 ? Uri.UnescapeDataString(kv[1] ?? string.Empty) : string.Empty;
                    if (!string.IsNullOrWhiteSpace(key))
                    {
                        query[key] = value;
                    }
                }
            }

            return new RequestTarget
            {
                Path = string.IsNullOrWhiteSpace(path) ? "/" : path,
                Query = query
            };
        }

        private sealed class RequestTarget
        {
            public string Path { get; set; }

            public Dictionary<string, string> Query { get; set; }
        }

        private static object ResolveTrackedState(PathAnimationManager animationManager, ModelItem controlledObject)
        {
            if (animationManager == null || controlledObject == null || !animationManager.ControlsAnimatedObject(controlledObject))
            {
                return new
                {
                    isAvailable = false,
                    reason = animationManager == null ? "PathAnimationManagerUnavailable" : "ControlledObjectUnavailable"
                };
            }

            var currentState = animationManager.GetObjectCurrentPosition(controlledObject);
            return new
            {
                isAvailable = true,
                position = SerializePoint3D(currentState.Position),
                yawRadians = currentState.Yaw,
                yawDegrees = currentState.Yaw * 180.0 / Math.PI,
                hasTrackedRotation = animationManager.HasTrackedRotation,
                trackedRotation = animationManager.HasTrackedRotation
                    ? SerializeRotation3D(animationManager.TrackedRotation)
                    : null
            };
        }

        private static object ResolveGeometryState(ModelItem controlledObject)
        {
            if (controlledObject == null)
            {
                return new
                {
                    isAvailable = false,
                    reason = "ControlledObjectUnavailable"
                };
            }

            bool hasGeometryRotation = ModelItemTransformHelper.TryGetCurrentGeometryRotation(controlledObject, out Rotation3D geometryRotation);
            bool hasOverrideRotation = ModelItemTransformHelper.TryGetCurrentOverrideRotation(controlledObject, out Rotation3D overrideRotation);

            return new
            {
                isAvailable = true,
                geometryRotation = hasGeometryRotation ? SerializeRotation3D(geometryRotation) : null,
                overrideRotation = hasOverrideRotation ? SerializeRotation3D(overrideRotation) : null
            };
        }

        private static object ResolveBoundingBoxState(ModelItem controlledObject)
        {
            if (controlledObject == null)
            {
                return new
                {
                    isAvailable = false,
                    reason = "ControlledObjectUnavailable"
                };
            }

            BoundingBox3D boundingBox = controlledObject.BoundingBox();
            return new
            {
                isAvailable = true,
                min = SerializePoint3D(boundingBox.Min),
                max = SerializePoint3D(boundingBox.Max),
                center = SerializePoint3D(boundingBox.Center)
            };
        }

        private static object SerializePoint3D(Point3D point)
        {
            if (point == null)
            {
                return null;
            }

            return new
            {
                x = point.X,
                y = point.Y,
                z = point.Z
            };
        }

        private static object SerializePathPoint(PathPoint point)
        {
            if (point == null)
            {
                return null;
            }

            return new
            {
                id = point.Id,
                name = point.Name,
                type = point.Type.ToString(),
                index = point.Index,
                position = SerializePoint3D(point.Position)
            };
        }

        private static object SerializeGridStats(GridMap gridMap)
        {
            if (gridMap == null)
            {
                return null;
            }

            int walkableCellCount = 0;
            int nonWalkableCellCount = 0;
            int obstacleCellCount = 0;
            int holeCellCount = 0;
            int boundaryLayerCount = 0;

            for (int x = 0; x < gridMap.Width; x++)
            {
                for (int y = 0; y < gridMap.Height; y++)
                {
                    GridCell cell = gridMap.Cells[x, y];
                    bool isWalkable = cell.HasAnyWalkableLayer();
                    if (isWalkable)
                    {
                        walkableCellCount++;
                    }
                    else
                    {
                        nonWalkableCellCount++;
                    }

                    if (string.Equals(cell.CellType, "障碍物", StringComparison.Ordinal))
                    {
                        obstacleCellCount++;
                    }

                    if (string.Equals(cell.CellType, "空洞", StringComparison.Ordinal))
                    {
                        holeCellCount++;
                    }

                    if (cell.HeightLayers != null)
                    {
                        boundaryLayerCount += cell.HeightLayers.Count(layer => layer.IsBoundary);
                    }
                }
            }

            int totalCellCount = gridMap.Width * gridMap.Height;

            return new
            {
                width = gridMap.Width,
                height = gridMap.Height,
                totalCellCount,
                walkableCellCount,
                nonWalkableCellCount,
                obstacleCellCount,
                holeCellCount,
                boundaryLayerCount,
                obstacleRatio = totalCellCount == 0 ? 0.0 : (double)obstacleCellCount / totalCellCount,
                nonWalkableRatio = totalCellCount == 0 ? 0.0 : (double)nonWalkableCellCount / totalCellCount
            };
        }

        private static object BuildObstacleDiagnostics(GridMapGenerator gridMapGenerator, GridMap gridMap, double objectHeightInMeters, double safetyMarginInMeters)
        {
            if (gridMapGenerator == null || gridMap == null)
            {
                return null;
            }

            double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units);
            double scanHeightInModelUnits = (objectHeightInMeters + safetyMarginInMeters) * metersToModelUnits;

            var traversableItems = CategoryAttributeManager.GetAllTraversableLogisticsItems().ToList();
            var irrelevantItems = CategoryAttributeManager.GetLogisticsItemsByType("无关项");

            var collectRelatedItemsMethod = typeof(GridMapGenerator).GetMethod(
                "CollectRelatedItems",
                System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
            var postProcessGeometryItemsMethod = typeof(GridMapGenerator).GetMethod(
                "PostProcessGeometryItems",
                System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
            var getObstacleElevationRangeMethod = typeof(GridMapGenerator).GetMethod(
                "GetObstacleElevationRange",
                System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,
                null,
                new[] { typeof(Point3D), typeof(Point3D), typeof(GridMap) },
                null);
            var calculateBoundingBoxGridCoverageMethod = typeof(GridMapGenerator).GetMethod(
                "CalculateBoundingBoxGridCoverage",
                System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);

            var traversableRelatedItems = (HashSet<ModelItem>)collectRelatedItemsMethod.Invoke(gridMapGenerator, new object[] { traversableItems });
            var irrelevantRelatedItems = (HashSet<ModelItem>)collectRelatedItemsMethod.Invoke(gridMapGenerator, new object[] { irrelevantItems });
            var geometryItems = ModelItemAnalysisHelper.GetAllVisibleGeometryItems();
            var itemCache = (Dictionary<ModelItem, ItemProperties>)postProcessGeometryItemsMethod.Invoke(
                gridMapGenerator,
                new object[] { geometryItems, traversableRelatedItems, irrelevantRelatedItems });

            var validItems = itemCache
                .Where(kvp => kvp.Value.HasGeometry &&
                              !kvp.Value.IsContainer &&
                              !kvp.Value.IsChannelItem &&
                              !kvp.Value.IsChildOfChannel &&
                              kvp.Value.BoundingBox != null &&
                              kvp.Value.IsInScanHeightRange)
                .ToList();

            double walkableMinElevation = double.MaxValue;
            double walkableMaxElevation = double.MinValue;
            int walkableGridCount = 0;
            for (int x = 0; x < gridMap.Width; x++)
            {
                for (int y = 0; y < gridMap.Height; y++)
                {
                    GridCell cell = gridMap.Cells[x, y];
                    if (!cell.HasAnyWalkableLayer() || cell.HeightLayers == null || cell.HeightLayers.Count == 0)
                    {
                        continue;
                    }

                    walkableGridCount++;
                    double cellElevation = cell.HeightLayers[0].Z;
                    walkableMinElevation = Math.Min(walkableMinElevation, cellElevation);
                    walkableMaxElevation = Math.Max(walkableMaxElevation, cellElevation);
                }
            }

            if (walkableGridCount == 0)
            {
                walkableMinElevation = 0.0;
                walkableMaxElevation = 0.0;
            }

            double scanMin = walkableMinElevation;
            double scanMax = walkableMaxElevation + scanHeightInModelUnits;

            var keptItems = new List<object>();
            var filteredSampleItems = new List<object>();
            int filteredByHeightCount = 0;

            foreach (var kvp in validItems)
            {
                BoundingBox3D bbox = kvp.Value.BoundingBox;
                var elevationRange = ((double min, double max))getObstacleElevationRangeMethod.Invoke(
                    gridMapGenerator,
                    new object[] { bbox.Min, bbox.Max, gridMap });

                bool isInRange = !(elevationRange.max <= scanMin || elevationRange.min > scanMax);
                if (!isInRange)
                {
                    filteredByHeightCount++;
                    if (filteredSampleItems.Count < 10)
                    {
                        filteredSampleItems.Add(new
                        {
                            displayName = kvp.Key.DisplayName,
                            bboxMin = SerializePoint3D(bbox.Min),
                            bboxMax = SerializePoint3D(bbox.Max),
                            minElevation = elevationRange.min,
                            maxElevation = elevationRange.max
                        });
                    }

                    continue;
                }

                if (keptItems.Count < 30)
                {
                    var coveredCells = (List<(int x, int y)>)calculateBoundingBoxGridCoverageMethod.Invoke(
                        gridMapGenerator,
                        new object[] { bbox, gridMap });

                    object triangleBounds = keptItems.Count < 8
                        ? SerializeTriangleBounds(kvp.Key)
                        : null;

                    keptItems.Add(new
                    {
                        displayName = kvp.Key.DisplayName,
                        bboxMin = SerializePoint3D(bbox.Min),
                        bboxMax = SerializePoint3D(bbox.Max),
                        triangleBounds,
                        minElevation = elevationRange.min,
                        maxElevation = elevationRange.max,
                        coveredCellCount = coveredCells.Count
                    });
                }
            }

            return new
            {
                geometryItemCount = geometryItems.Count,
                traversableItemCount = traversableItems.Count,
                traversableRelatedItemCount = traversableRelatedItems.Count,
                irrelevantItemCount = irrelevantItems.Count,
                irrelevantRelatedItemCount = irrelevantRelatedItems.Count,
                postProcessedItemCount = itemCache.Count,
                validItemCount = validItems.Count,
                walkableGridCount,
                walkableMinElevation,
                walkableMaxElevation,
                scanHeightInModelUnits,
                scanMin,
                scanMax,
                filteredByHeightCount,
                keptItemSample = keptItems,
                filteredByHeightSample = filteredSampleItems
            };
        }

        private static object SerializeTriangleBounds(ModelItem item)
        {
            if (item == null)
            {
                return null;
            }

            List<Triangle3D> triangles = GeometryHelper.ExtractTriangles(new[] { item });
            if (triangles == null || triangles.Count == 0)
            {
                return null;
            }

            double minX = triangles.Min(t => Math.Min(t.Point1.X, Math.Min(t.Point2.X, t.Point3.X)));
            double minY = triangles.Min(t => Math.Min(t.Point1.Y, Math.Min(t.Point2.Y, t.Point3.Y)));
            double minZ = triangles.Min(t => Math.Min(t.Point1.Z, Math.Min(t.Point2.Z, t.Point3.Z)));
            double maxX = triangles.Max(t => Math.Max(t.Point1.X, Math.Max(t.Point2.X, t.Point3.X)));
            double maxY = triangles.Max(t => Math.Max(t.Point1.Y, Math.Max(t.Point2.Y, t.Point3.Y)));
            double maxZ = triangles.Max(t => Math.Max(t.Point1.Z, Math.Max(t.Point2.Z, t.Point3.Z)));

            return new
            {
                min = SerializePoint3D(new Point3D(minX, minY, minZ)),
                max = SerializePoint3D(new Point3D(maxX, maxY, maxZ))
            };
        }

        private static T InvokePathManagerPrivate<T>(PathPlanningManager pathManager, string methodName, params object[] args)
        {
            var method = typeof(PathPlanningManager).GetMethod(
                methodName,
                System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);

            if (method == null)
            {
                throw new MissingMethodException(typeof(PathPlanningManager).FullName, methodName);
            }

            object result = method.Invoke(pathManager, args);
            if (result == null)
            {
                return default(T);
            }

            return (T)result;
        }

        private static object SerializeRotation3D(Rotation3D rotation)
        {
            var linear = new Transform3D(rotation).Linear;
            return new
            {
                quaternion = new
                {
                    x = rotation.A,
                    y = rotation.B,
                    z = rotation.C,
                    w = rotation.D
                },
                axes = new
                {
                    hostX = new
                    {
                        x = linear.Get(0, 0),
                        y = linear.Get(1, 0),
                        z = linear.Get(2, 0)
                    },
                    hostY = new
                    {
                        x = linear.Get(0, 1),
                        y = linear.Get(1, 1),
                        z = linear.Get(2, 1)
                    },
                    hostZ = new
                    {
                        x = linear.Get(0, 2),
                        y = linear.Get(1, 2),
                        z = linear.Get(2, 2)
                    }
                }
            };
        }

        private static string WriteSnapshotToDisk(object snapshotPayload)
        {
            string logDirectory = Path.GetDirectoryName(LogManager.LogFilePath);
            string snapshotDirectory = Path.Combine(logDirectory ?? AppDomain.CurrentDomain.BaseDirectory, "test-automation");
            Directory.CreateDirectory(snapshotDirectory);

            string fileName = string.Format(
                CultureInfo.InvariantCulture,
                "debug-snapshot-{0:yyyyMMdd-HHmmss-fff}.json",
                DateTime.Now);
            string fullPath = Path.Combine(snapshotDirectory, fileName);
            string json = JsonConvert.SerializeObject(snapshotPayload, Formatting.Indented);
            File.WriteAllText(fullPath, json, new UTF8Encoding(false));

            LogManager.Info($"[测试HTTP] 已导出调试快照: {fullPath}");
            return fullPath;
        }

        private static AnimationControlViewModel RequireAnimationControlViewModel()
        {
            var animationViewModel = AnimationControlViewModel.Instance;
            if (animationViewModel == null)
            {
                throw new InvalidOperationException("AnimationControlViewModel 当前不可用，请先打开插件动画页签");
            }

            return animationViewModel;
        }

        private static PreparedVirtualCollisionTest PrepareVirtualCollisionTest(Dictionary<string, string> query, PathType? fixedPathType)
        {
            PathPlanningManager pathManager = RequirePathManager();
            AnimationControlViewModel animationViewModel = RequireAnimationControlViewModel();
            animationViewModel.SetPathPlanningManager(pathManager);

            ApplyTestAnimationParameters(animationViewModel, query);

            PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, fixedPathType);
            pathManager.SetCurrentRoute(route);
            animationViewModel.SetCurrentPath(CreatePathRouteViewModel(route));

            // 清除上一次测试残留的碰撞报告，避免 HasGeneratedCollisionReport/LastGeneratedReport 跨测试串扰
            animationViewModel.InvalidateLastGeneratedReport();

            animationViewModel.IsManualCollisionTargetEnabled = false;

            bool useVirtualObject = ParseBooleanQuery(query, "useVirtualObject", true);
            object animatedObjectPayload;

            if (useVirtualObject)
            {
                animationViewModel.UseVirtualObject = true;
                animatedObjectPayload = new
                {
                    mode = "VirtualObject",
                    displayName = "虚拟物体"
                };
            }
            else
            {
                // 真实物体模式：按名称/当前选择集选择真实物体
                ModelItem realObject = ResolveAnimatedObjectFromQuery(query);
                SelectDocumentItem(realObject);

                if (!animationViewModel.SelectAnimatedObjectCommand.CanExecute(null))
                {
                    throw new InvalidOperationException("当前动画视图模型不允许执行“选择移动物体”命令");
                }

                animationViewModel.UseVirtualObject = false;
                animationViewModel.SelectAnimatedObjectCommand.Execute(null);

                if (!IsSameModelItem(animationViewModel.SelectedAnimatedObject, realObject))
                {
                    string detail = animationViewModel.SelectedAnimatedObject == null
                        ? "选择真实物体失败：物体在当前文档不可用（fragment 姿态解析失败，常见于导出局部模型中的嵌套实例物体；请改用虚拟物体或单层结构物体）"
                        : "选择真实物体失败：动画视图模型未保留选中的对象";
                    throw new InvalidOperationException(detail);
                }

                animatedObjectPayload = new
                {
                    mode = "RealObject",
                    displayName = realObject.DisplayName
                };
            }

            if (!animationViewModel.CanGenerateAnimation || !animationViewModel.GenerateAnimationCommand.CanExecute(null))
            {
                throw new InvalidOperationException($"{route.PathType} 碰撞测试准备失败：当前条件下不能生成动画");
            }

            animationViewModel.GenerateAnimationCommand.Execute(null);

            LogManager.Info(
                $"[测试HTTP] 已开始准备{(useVirtualObject ? "虚拟物体" : "真实物体")}碰撞测试: 路径={route.Name}, 类型={route.PathType}");

            return new PreparedVirtualCollisionTest
            {
                pathType = route.PathType,
                route = SerializeRoute(route, route),
                animatedObject = animatedObjectPayload
            };
        }

        /// <summary>
        /// 动画逐帧状态探针：生成动画后 seek 到指定进度点，返回各点物体跟踪位置/yaw。
        /// 供集成测试验证物体沿路径移动（起点→中间→终点）与平面姿态链。
        /// </summary>
        /// <summary>
        /// 动画播放控制集成测试序列：prepare → start → pause（验证帧停止）→ resume（验证帧前进）→ finished。
        /// 验证 PathAnimationManager 的暂停/恢复/播放完整链路。
        /// </summary>
        private static async Task<object> AnimationPlaybackControlAsync(Dictionary<string, string> query)
        {
            await _testExecutionLock.WaitAsync().ConfigureAwait(false);
            try
            {
                return await AnimationPlaybackControlCoreAsync(query).ConfigureAwait(false);
            }
            finally
            {
                _testExecutionLock.Release();
            }
        }

        private static async Task<object> AnimationPlaybackControlCoreAsync(Dictionary<string, string> query)
        {
            int timeoutSeconds = ParseTimeoutSeconds(query, 180);
            DateTime deadlineUtc = DateTime.UtcNow.AddSeconds(timeoutSeconds);

            PathRoute route = InvokeOnUiThread(() => PrepareProbeAnimation(query));

            await WaitForConditionAsync(
                deadlineUtc,
                () => InvokeOnUiThread(() =>
                {
                    var animationManager = PathAnimationManager.GetInstance();
                    return animationManager != null &&
                           animationManager.TotalFrames > 0 &&
                           (animationManager.CurrentState == AnimationState.Ready ||
                            animationManager.CurrentState == AnimationState.Finished);
                }),
                "等待动画生成完成超时").ConfigureAwait(false);

            var stages = new List<object>();
            int totalFrames = 0;

            // 1. 开始播放
            InvokeOnUiThread(() =>
            {
                StartPreparedAnimation();
                return true;
            });

            // 2. 等待播放开始（帧前进）
            await WaitForConditionAsync(
                deadlineUtc,
                () => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentFrame > 0),
                "等待动画开始播放超时").ConfigureAwait(false);

            totalFrames = InvokeOnUiThread(() => PathAnimationManager.GetInstance().TotalFrames);
            int playingFrame = InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame);
            stages.Add(RecordStage("playing", playingFrame));

            // 等待至少前进一帧，确保暂停观测窗口有效（帧间隔 ~66ms @15fps）
            await WaitForConditionAsync(
                deadlineUtc,
                () => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentFrame > playingFrame),
                "等待播放前进超时").ConfigureAwait(false);

            // 3. 暂停并验证帧停止
            InvokeOnUiThread(() =>
            {
                PathAnimationManager.GetInstance().PauseAnimation();
                return true;
            });

            int pausedFrame = InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame);
            stages.Add(RecordStage("paused", pausedFrame));

            // 暂停期间等待 1.2 秒，帧不应前进
            await Task.Delay(1200).ConfigureAwait(false);
            int frameAfterPause = InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame);
            bool pauseVerified = frameAfterPause == pausedFrame;

            // 4. 恢复播放，帧继续前进
            InvokeOnUiThread(() =>
            {
                PathAnimationManager.GetInstance().ResumeAnimation();
                return true;
            });

            await WaitForConditionAsync(
                deadlineUtc,
                () => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentFrame > pausedFrame),
                "等待动画恢复播放超时").ConfigureAwait(false);
            stages.Add(RecordStage("resumed", InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame)));

            // 5. 等待播放完成
            await WaitForConditionAsync(
                deadlineUtc,
                () => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentState == AnimationState.Finished),
                "等待动画播放完成超时").ConfigureAwait(false);
            stages.Add(RecordStage("finished", InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame)));

            return new
            {
                route = SerializeRoute(route, route),
                totalFrames,
                pauseVerified,
                pausedFrame,
                frameAfterPause,
                stages
            };
        }

        private static object RecordStage(string stage, int frame)
        {
            return new
            {
                stage,
                frame,
                state = PathAnimationManager.GetInstance()?.CurrentState.ToString()
            };
        }

        private static async Task<object> ProbeAnimationFramesAsync(Dictionary<string, string> query)
        {
            await _testExecutionLock.WaitAsync().ConfigureAwait(false);
            try
            {
                return await ProbeAnimationFramesCoreAsync(query).ConfigureAwait(false);
            }
            finally
            {
                _testExecutionLock.Release();
            }
        }

        private static async Task<object> ProbeAnimationFramesCoreAsync(Dictionary<string, string> query)
        {
            int timeoutSeconds = ParseTimeoutSeconds(query, 180);
            DateTime deadlineUtc = DateTime.UtcNow.AddSeconds(timeoutSeconds);

            PathRoute route = InvokeOnUiThread(() => PrepareProbeAnimation(query));

            await WaitForConditionAsync(
                deadlineUtc,
                () => InvokeOnUiThread(() =>
                {
                    var animationManager = PathAnimationManager.GetInstance();
                    return animationManager != null &&
                           animationManager.TotalFrames > 0 &&
                           (animationManager.CurrentState == AnimationState.Ready ||
                            animationManager.CurrentState == AnimationState.Finished);
                }),
                "等待动画生成完成超时").ConfigureAwait(false);

            return InvokeOnUiThread(() =>
            {
                var animationManager = PathAnimationManager.GetInstance();
                var controlledObject = VirtualObjectManager.Instance.CurrentVirtualObject;
                if (animationManager == null || controlledObject == null)
                {
                    throw new InvalidOperationException("虚拟物体或动画管理器当前不可用，无法执行帧探针");
                }

                int totalFrames = animationManager.TotalFrames;
                string probeText = GetOptionalQueryValue(query, "probeProgress") ?? "0,0.5,1.0";
                var probes = new List<object>();

                foreach (string part in probeText.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
                {
                    if (!double.TryParse(part, NumberStyles.Float, CultureInfo.InvariantCulture, out double progress))
                    {
                        throw new InvalidOperationException($"无法解析探针进度: {part}");
                    }

                    progress = Math.Max(0.0, Math.Min(1.0, progress));
                    animationManager.SeekToProgress(progress);

                    var (position, yaw) = animationManager.GetObjectCurrentPosition(controlledObject);
                    probes.Add(new
                    {
                        progress,
                        frameIndex = animationManager.CurrentFrame,
                        position = SerializePoint3D(position),
                        yawRadians = yaw,
                        yawDegrees = yaw * 180.0 / Math.PI
                    });
                }

                List<PathPoint> orderedPoints = route.Points?.OrderBy(p => p.Index).ToList() ?? new List<PathPoint>();
                return new
                {
                    route = SerializeRoute(route, route),
                    totalFrames,
                    pathStart = SerializePoint3D(orderedPoints.FirstOrDefault()?.Position),
                    pathEnd = SerializePoint3D(orderedPoints.LastOrDefault()?.Position),
                    probes
                };
            });
        }

        /// <summary>
        /// 生成虚拟物体动画（帧探针准备阶段），返回当前路径。
        /// </summary>
        private static PathRoute PrepareProbeAnimation(Dictionary<string, string> query)
        {
            PathPlanningManager pathManager = RequirePathManager();
            AnimationControlViewModel animationViewModel = RequireAnimationControlViewModel();
            animationViewModel.SetPathPlanningManager(pathManager);

            ApplyTestAnimationParameters(animationViewModel, query);

            PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, null);
            pathManager.SetCurrentRoute(route);
            animationViewModel.SetCurrentPath(CreatePathRouteViewModel(route));

            // 清除上一次测试残留的碰撞报告（帧探针场景同样避免跨测试串扰）
            animationViewModel.InvalidateLastGeneratedReport();

            animationViewModel.IsManualCollisionTargetEnabled = false;
            animationViewModel.UseVirtualObject = true;

            if (!animationViewModel.CanGenerateAnimation || !animationViewModel.GenerateAnimationCommand.CanExecute(null))
            {
                throw new InvalidOperationException($"{route.PathType} 帧探针测试准备失败：当前条件下不能生成动画");
            }

            animationViewModel.GenerateAnimationCommand.Execute(null);
            LogManager.Info($"[测试HTTP] 帧探针测试动画已开始生成: 路径={route.Name}, 类型={route.PathType}");
            return route;
        }

        /// <summary>
        /// 应用测试动画参数（帧率/时长），加速集成测试：
        /// query 支持 frameRate（FPS）与 durationSeconds，缺省保持配置默认值。
        /// </summary>
        private static void ApplyTestAnimationParameters(AnimationControlViewModel animationViewModel, Dictionary<string, string> query)
        {
            if (int.TryParse(GetOptionalQueryValue(query, "frameRate"), out int frameRate) && frameRate > 0)
            {
                animationViewModel.SelectedFrameRate = frameRate;
            }

            if (double.TryParse(GetOptionalQueryValue(query, "durationSeconds"), NumberStyles.Float, CultureInfo.InvariantCulture, out double durationSeconds) &&
                durationSeconds > 0)
            {
                animationViewModel.AnimationDuration = durationSeconds;
            }
        }

        private static void StartPreparedAnimation()
        {
            AnimationControlViewModel animationViewModel = RequireAnimationControlViewModel();
            PathAnimationManager animationManager = PathAnimationManager.GetInstance();
            if (animationManager == null)
            {
                throw new InvalidOperationException("PathAnimationManager 当前不可用，无法开始播放");
            }

            if (animationManager.CurrentState == AnimationState.Paused)
            {
                animationManager.ResumeAnimation();
                LogManager.Info("[测试HTTP] 已从暂停状态恢复自动测试动画播放");
                return;
            }

            if (animationManager.IsAnimating)
            {
                LogManager.Info("[测试HTTP] 动画已经在播放中，跳过重复开始");
                return;
            }

            if (animationManager.CurrentState != AnimationState.Ready &&
                animationManager.CurrentState != AnimationState.Finished)
            {
                throw new InvalidOperationException($"动画尚未就绪，当前状态={animationManager.CurrentState}");
            }

            animationManager.ClearExclusionCache();
            ModelHighlightHelper.ClearCollisionHighlights();
            animationManager.StartAnimation();
            LogManager.Info("[测试HTTP] 已启动自动测试动画播放");
        }

        private static PathRoute ResolveVirtualCollisionRoute(PathPlanningManager pathManager, Dictionary<string, string> query, PathType? fixedPathType)
        {
            string routeName = GetOptionalQueryValue(query, "routeName");
            string pathTypeText = fixedPathType.HasValue
                ? fixedPathType.Value.ToString()
                : GetOptionalQueryValue(query, "pathType");
            List<PathRoute> routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
            PathType pathType = fixedPathType ?? ParsePathTypeOrThrow(pathTypeText, "pathType");

            string routePrefix = GetOptionalQueryValue(query, "routePrefix")
                                 ?? GetOptionalQueryValue(query, "prefix")
                                 ?? DefaultAutoTestRoutePrefix;

            PathRoute route = string.IsNullOrWhiteSpace(routeName)
                ? TryFindAutoTestRoute(routes, pathType, routePrefix)
                : routes.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));

            if (route == null)
            {
                // 集成测试依赖的标准路径缺失时，自动从插件资源文件导入后重试。
                // 这是唯一允许的自动导入路径（仅限标准测试路径，不影响用户数据）。
                EnsureAutoTestRoutesImported(pathManager);

                routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
                route = string.IsNullOrWhiteSpace(routeName)
                    ? TryFindAutoTestRoute(routes, pathType, routePrefix)
                    : routes.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
            }

            if (route == null)
            {
                throw new InvalidOperationException(
                    string.IsNullOrWhiteSpace(routeName)
                        ? $"找不到默认自动测试路径: prefix={routePrefix}, pathType={pathType}"
                        : $"找不到指定路径: {routeName}");
            }

            if (route.PathType != pathType)
            {
                throw new InvalidOperationException($"指定路径类型不匹配: 期望={pathType}, 实际={route.PathType}, 路径={route.Name}");
            }

            return route;
        }

        /// <summary>
        /// 判断两个 ModelItem 是否指向同一模型对象。
        /// NW API 中同一逻辑对象通过不同途径（Selection/Descendants）获取的实例不保证同一引用，
        /// 因此用 InstanceGuid 比较，禁止用 ReferenceEquals。
        /// </summary>
        private static bool IsSameModelItem(ModelItem a, ModelItem b)
        {
            if (a == null || b == null)
            {
                return false;
            }

            return string.Equals(
                a.InstanceGuid.ToString(),
                b.InstanceGuid.ToString(),
                StringComparison.OrdinalIgnoreCase);
        }

        private static PathType ParsePathTypeOrThrow(string value, string parameterName)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new InvalidOperationException($"缺少必填参数: {parameterName}");
            }

            if (!Enum.TryParse(value, true, out PathType pathType))
            {
                throw new InvalidOperationException($"无法解析路径类型: {value}");
            }

            return pathType;
        }

        /// <summary>
        /// 检查并导入集成测试标准路径（自动测试_ 前缀 + 测试基准）。
        /// 仅当对应名称的路径缺失时，从插件部署目录 resources\auto-test-routes.xml 导入。
        /// 已存在或重名的路径不会被覆盖/重命名，确保不破坏用户数据。
        /// </summary>
        private static void EnsureAutoTestRoutesImported(PathPlanningManager pathManager)
        {
            string[] standardRouteNames = new[]
            {
                "自动测试_Ground",
                "自动测试_Hoisting",
                "自动测试_Rail",
                "自动测试_Free",
                "测试基准"
            };

            List<PathRoute> existingRoutes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
            List<string> missingNames = standardRouteNames
                .Where(name => !existingRoutes.Any(route => string.Equals(route.Name, name, StringComparison.Ordinal)))
                .ToList();

            if (missingNames.Count == 0)
            {
                return;
            }

            string pluginDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
            string resourcePath = Path.Combine(pluginDirectory ?? string.Empty, "resources", AutoTestRoutesResourceFileName);

            if (!File.Exists(resourcePath))
            {
                throw new InvalidOperationException(
                    $"缺少标准测试路径资源文件: {resourcePath}。请确认插件已完整部署（resources\\{AutoTestRoutesResourceFileName}）。缺失路径: {string.Join(", ", missingNames)}");
            }

            List<PathRoute> importedRoutes = new PathDataManager().ImportFromXml(resourcePath);
            if (importedRoutes == null || importedRoutes.Count == 0)
            {
                throw new InvalidOperationException($"标准测试路径资源文件为空: {resourcePath}");
            }

            int importedCount = 0;
            foreach (PathRoute importedRoute in importedRoutes)
            {
                bool exists = existingRoutes.Any(route => string.Equals(route.Name, importedRoute.Name, StringComparison.Ordinal));
                if (exists)
                {
                    continue;
                }

                if (importedRoute.PathType != PathType.Ground &&
                    importedRoute.PathType != PathType.Hoisting &&
                    importedRoute.PathType != PathType.Rail &&
                    importedRoute.PathType != PathType.Free)
                {
                    continue;
                }

                if (pathManager.AddRoute(importedRoute))
                {
                    importedCount++;
                    LogManager.Info($"[测试HTTP] 已导入标准测试路径: {importedRoute.Name} ({importedRoute.PathType})");
                }
            }

            if (importedCount == 0)
            {
                throw new InvalidOperationException(
                    $"标准测试路径导入失败（0 条成功）。缺失路径: {string.Join(", ", missingNames)}");
            }

            LogManager.Info($"[测试HTTP] 标准测试路径检查完成: 缺失 {missingNames.Count} 条，已导入 {importedCount} 条");
        }

        private static ModelItem ResolveAnimatedObjectFromQuery(Dictionary<string, string> query)
        {
            Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
            if (activeDocument == null)
            {
                throw new InvalidOperationException("当前没有活动文档，无法选择真实物体");
            }

            string animatedObjectName = GetOptionalQueryValue(query, "animatedObjectName");
            string animatedObjectPath = GetOptionalQueryValue(query, "animatedObjectPath");

            if (string.IsNullOrWhiteSpace(animatedObjectName) && string.IsNullOrWhiteSpace(animatedObjectPath))
            {
                return ResolveSingleSelectedItem(activeDocument);
            }

            var matches = new List<ModelItem>();
            foreach (Model model in activeDocument.Models)
            {
                if (model?.RootItem == null)
                {
                    continue;
                }

                foreach (ModelItem item in model.RootItem.DescendantsAndSelf)
                {
                    if (item == null)
                    {
                        continue;
                    }

                    bool nameMatches = !string.IsNullOrWhiteSpace(animatedObjectName) &&
                                       string.Equals(item.DisplayName, animatedObjectName, StringComparison.OrdinalIgnoreCase);

                    bool pathMatches = false;
                    if (!string.IsNullOrWhiteSpace(animatedObjectPath))
                    {
                        try
                        {
                            var pathId = activeDocument.Models.CreatePathId(item);
                            pathMatches = string.Equals(pathId.PathId, animatedObjectPath, StringComparison.OrdinalIgnoreCase);
                        }
                        catch
                        {
                            // 忽略单点路径解析失败，继续匹配
                        }
                    }

                    if (nameMatches || pathMatches)
                    {
                        matches.Add(item);
                    }
                }
            }

            if (matches.Count == 0)
            {
                string target = string.IsNullOrWhiteSpace(animatedObjectPath) ? animatedObjectName : animatedObjectPath;
                throw new InvalidOperationException($"找不到指定真实物体: {target}");
            }

            if (matches.Count > 1)
            {
                string target = string.IsNullOrWhiteSpace(animatedObjectPath) ? animatedObjectName : animatedObjectPath;
                throw new InvalidOperationException($"找到多个匹配真实物体，请改用更唯一的名字或 PathId: {target}");
            }

            return matches[0];
        }

        private static ModelItem ResolveSingleSelectedItem(Document activeDocument)
        {
            var selectedItems = activeDocument.CurrentSelection?.SelectedItems?.Cast<ModelItem>().Where(item => item != null).ToList()
                               ?? new List<ModelItem>();

            if (selectedItems.Count != 1)
            {
                throw new InvalidOperationException($"当前选择集必须且只能包含 1 个真实物体，当前数量: {selectedItems.Count}");
            }

            return selectedItems[0];
        }

        private static void SelectDocumentItem(ModelItem item)
        {
            Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
            if (activeDocument?.CurrentSelection == null)
            {
                throw new InvalidOperationException("当前文档选择集不可用");
            }

            activeDocument.CurrentSelection.Clear();
            activeDocument.CurrentSelection.Add(item);
        }

        private static PathRouteViewModel CreatePathRouteViewModel(PathRoute route)
        {
            var routeViewModel = new PathRouteViewModel(isFromDatabase: true)
            {
                Route = route,
                IsActive = true
            };

            foreach (var point in route.Points.OrderBy(p => p.Index))
            {
                routeViewModel.Points.Add(new PathPointViewModel
                {
                    Id = point.Id,
                    Name = point.Name,
                    Type = point.Type,
                    Index = point.Index,
                    X = point.X,
                    Y = point.Y,
                    Z = point.Z
                });
            }

            routeViewModel.SetTimeInfo(route.CreatedTime, route.LastModified);
            return routeViewModel;
        }

        private static int ParseTimeoutSeconds(Dictionary<string, string> query, int defaultSeconds)
        {
            string raw = GetOptionalQueryValue(query, "timeoutSeconds");
            if (string.IsNullOrWhiteSpace(raw))
            {
                return defaultSeconds;
            }

            if (!int.TryParse(raw, out int parsedSeconds) || parsedSeconds <= 0)
            {
                throw new InvalidOperationException($"无法解析 timeoutSeconds: {raw}");
            }

            return parsedSeconds;
        }

        private static bool ParseBooleanQuery(Dictionary<string, string> query, string key, bool defaultValue)
        {
            string raw = GetOptionalQueryValue(query, key);
            if (string.IsNullOrWhiteSpace(raw))
            {
                return defaultValue;
            }

            if (bool.TryParse(raw, out bool parsed))
            {
                return parsed;
            }

            if (string.Equals(raw, "1", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(raw, "yes", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(raw, "y", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (string.Equals(raw, "0", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(raw, "no", StringComparison.OrdinalIgnoreCase) ||
                string.Equals(raw, "n", StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            throw new InvalidOperationException($"无法解析布尔参数 {key}: {raw}");
        }

        private static IDisposable EnableAutoConfirmCollisionAnalysisDialogs()
        {
            Interlocked.Increment(ref _autoConfirmCollisionAnalysisDialogRequestCount);
            return new ActionOnDispose(() =>
            {
                if (Interlocked.Decrement(ref _autoConfirmCollisionAnalysisDialogRequestCount) < 0)
                {
                    Interlocked.Exchange(ref _autoConfirmCollisionAnalysisDialogRequestCount, 0);
                }
            });
        }

        private static IDisposable EnableAutoChooseCreateNewDetectionRecord()
        {
            Interlocked.Increment(ref _autoChooseCreateNewDetectionRecordRequestCount);
            return new ActionOnDispose(() =>
            {
                if (Interlocked.Decrement(ref _autoChooseCreateNewDetectionRecordRequestCount) < 0)
                {
                    Interlocked.Exchange(ref _autoChooseCreateNewDetectionRecordRequestCount, 0);
                }
            });
        }

        private static async Task WaitForConditionAsync(DateTime deadlineUtc, Func<bool> condition, string timeoutMessage)
        {
            while (DateTime.UtcNow < deadlineUtc)
            {
                if (condition())
                {
                    return;
                }

                await Task.Delay(300).ConfigureAwait(false);
            }

            throw new TimeoutException(timeoutMessage);
        }

        private sealed class PreparedVirtualCollisionTest
        {
            public PathType pathType { get; set; }

            public object route { get; set; }

            public object animatedObject { get; set; }
        }

        private sealed class ActionOnDispose : IDisposable
        {
            private Action _disposeAction;

            public ActionOnDispose(Action disposeAction)
            {
                _disposeAction = disposeAction;
            }

            public void Dispose()
            {
                Action disposeAction = Interlocked.Exchange(ref _disposeAction, null);
                disposeAction?.Invoke();
            }
        }

        private static async Task WriteJsonResponseAsync(StreamWriter writer, int statusCode, object payload)
        {
            string statusText = ResolveStatusText(statusCode);
            string json = JsonConvert.SerializeObject(payload, Formatting.Indented);
            byte[] bodyBytes = Encoding.UTF8.GetBytes(json);

            await writer.WriteLineAsync($"HTTP/1.1 {statusCode} {statusText}").ConfigureAwait(false);
            await writer.WriteLineAsync("Content-Type: application/json; charset=utf-8").ConfigureAwait(false);
            await writer.WriteLineAsync($"Content-Length: {bodyBytes.Length}").ConfigureAwait(false);
            await writer.WriteLineAsync("Connection: close").ConfigureAwait(false);
            await writer.WriteLineAsync().ConfigureAwait(false);
            await writer.FlushAsync().ConfigureAwait(false);

            Stream baseStream = writer.BaseStream;
            await baseStream.WriteAsync(bodyBytes, 0, bodyBytes.Length).ConfigureAwait(false);
            await baseStream.FlushAsync(cancellationToken: CancellationToken.None).ConfigureAwait(false);
        }

        private static string ResolveStatusText(int statusCode)
        {
            switch (statusCode)
            {
                case 200:
                    return "OK";
                case 400:
                    return "Bad Request";
                case 404:
                    return "Not Found";
                case 405:
                    return "Method Not Allowed";
                case 500:
                    return "Internal Server Error";
                default:
                    return "OK";
            }
        }

        public void Dispose()
        {
            Stop();
        }
    }
}
