using LessokajiWeaverUtilities.Utilities;
using Simple3;
using Simple3.RCS;
using Simple3.RCS.CarTypes;
using Simple3.Props;
using Simple3.UI;
using SimpleCore;
using SimpleCore.Compiler;
using SimpleCore.Library;
using SimpleCore.PropType;
using SimpleCore.Traffic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using SimpleCore.BasicProps;

namespace StandardScene.CarTypes
{
    /// <summary>
    /// 基于 FASS 1.0 小车 TCP 协议实现的磁导航小车类型。
    /// 该类型直接按 32 字节协议帧与现场 AGV 服务端通讯，通过 <see cref="MagCarInterface"/> 执行调度脚本。
    /// 默认使用长连接发送命令和读取状态；现场服务端不支持长连接时，可关闭 <see cref="UsePersistentConnection"/> 回退到短连接兼容模式。
    /// </summary>
    [TemplateTrackCoderSettings(
        priority = 0,
        templateString = "agv.MagCarGo(${src.id},${dst.id},${track.Speed});",
        trackFields = typeof(BasicTrackFields))]
    [CarType(Name = "MagCar")]
    [I18N.DocumentTranslation(Name = "MagCar", locale = "en")]
    [EnvelopConfig(lengthX = 1200, lengthY = 800, centerX = 0, centerY = 0)]
    public class MagCar : GhostCar
    {
        /// <summary>
        /// FASS 1.0 协议帧起始字节，所有发送和接收帧第 0 字节均应为 0xBB。
        /// </summary>
        private const byte Begin = 0xBB;

        /// <summary>
        /// FASS 1.0 协议帧结束字节，所有发送和接收帧第 31 字节均应为 0xEE。
        /// </summary>
        private const byte End = 0xEE;

        /// <summary>
        /// 查询 AGV 当前状态的命令码，响应中会带回节点、报警、状态、电量、速度、角度等信息。
        /// </summary>
        private const byte CmdQueryState = 0x00;

        /// <summary>
        /// 启动或解除停止状态的命令码，数据区通常携带当前车头角度。
        /// </summary>
        private const byte CmdStart = 0x01;

        /// <summary>
        /// 停止命令码，数据区可携带停止保持秒数；当前实现通常传 0 表示立即停止。
        /// </summary>
        private const byte CmdStop = 0x02;

        /// <summary>
        /// 设置速度百分比的命令码，数据区第一个字节为 0-100 的速度百分比。
        /// </summary>
        private const byte CmdSpeed = 0x03;

        /// <summary>
        /// 设置车头角度的命令码，数据区携带角度值。
        /// </summary>
        private const byte CmdAngle = 0x04;

        /// <summary>
        /// 旧版节点下发命令码，只下发目标节点，用于兼容不支持 0xA1 任务命令的现场服务端。
        /// </summary>
        private const byte CmdLegacyNode = 0x05;

        /// <summary>
        /// FASS 1.0 任务下发命令码，数据区携带起点节点、终点节点和任务动作。
        /// </summary>
        private const byte CmdTask = 0xA1;

        /// <summary>
        /// TCP 发送、接收和长连接重连的同步锁。
        /// 多线程调度、keepAlive 和人工方法可能同时触发通讯，统一加锁可避免多个线程同时写同一个 Socket。
        /// </summary>
        private readonly object _syncRoot = new object();

        /// <summary>
        /// 长连接模式下复用的 TCP 客户端。
        /// 发送或读取失败时会关闭并置空，下一次命令会自动重新连接。
        /// </summary>
        private TcpClient _persistentClient;

        /// <summary>
        /// 最近一次 keepAlive 实际查询小车状态的时间。
        /// 外部框架可能高频调用 keepAlive，本字段用于按 <see cref="PollIntervalMs"/> 节流实际 TCP 查询频率。
        /// </summary>
        private DateTime _lastPollTime = DateTime.MinValue;

        /// <summary>
        /// AGV 服务端监听端口，默认 5000。
        /// </summary>
        [FieldMember] public int Port = 5000;

        /// <summary>
        /// 协议中的车辆编号，写入发送帧第 2-3 字节，使用大端字节序。
        /// 多车共用同一服务端时用于区分车辆。
        /// </summary>
        [FieldMember] public ushort VehicleCode = 0;

        /// <summary>
        /// TCP 建连超时时间，单位毫秒。
        /// 长连接首次连接或重连、短连接每次命令连接时都会使用该值。
        /// </summary>
        [FieldMember] public int ConnectTimeoutMs = 500;

        /// <summary>
        /// TCP 发送超时时间，单位毫秒。
        /// </summary>
        [FieldMember] public int SendTimeoutMs = 500;

        /// <summary>
        /// TCP 接收超时时间，单位毫秒。
        /// 读取响应时会一直读满 32 字节，超过该时间仍未收到完整帧则抛出异常。
        /// </summary>
        [FieldMember] public int ReceiveTimeoutMs = 500;

        /// <summary>
        /// keepAlive 中实际查询 AGV 状态的最小间隔，单位毫秒。
        /// 默认 1000，表示最多每秒向小车查询一次状态。
        /// </summary>
        [FieldMember] public int PollIntervalMs = 1000;

        /// <summary>
        /// 移动任务的预留超时时间，单位秒。
        /// 当前 MagCar 不下发脚本，保留该字段用于后续任务调度或界面配置兼容。
        /// </summary>
        [FieldMember] public int MoveTimeoutSeconds = 120;

        /// <summary>
        /// 是否使用 0xA1 任务命令下发起点和终点。
        /// 为 false 时改用 0x05 旧版目标节点命令，适配早期或定制 AGV 服务端。
        /// </summary>
        [FieldMember] public bool UseTaskCommand = true;

        /// <summary>
        /// 移动前是否先发送启动命令。
        /// 当前文件保留该配置以兼容调度策略，具体移动流程可在后续路径执行逻辑中使用。
        /// </summary>
        [FieldMember] public bool StartBeforeMove = true;

        /// <summary>
        /// 是否按接口文档使用 ushort 大端字段写入角度、节点、停止秒数等数据。
        /// 为 false 时只写入低 1 字节，用于兼容旧版 0x05 单字节节点协议。
        /// </summary>
        [FieldMember] public bool UseDocumentUShortFields = true;

        /// <summary>
        /// 是否使用站点字段 TagValue 作为 AGV 节点号。
        /// 为 true 时，收到的 Node 会先匹配站点 fields["TagValue"]；为 false 时直接用站点 id 匹配。
        /// </summary>
        [FieldMember] public bool UseTagValueAsNode = false;

        /// <summary>
        /// 是否启用 TCP 长连接。
        /// 默认 true；若现场 AGV 服务端不支持同一连接多次收发，可设为 false 回退到每条命令新建一次短连接。
        /// </summary>
        [FieldMember] public bool UsePersistentConnection = true;

        /// <summary>
        /// 是否输出 MagCar 详细日志。
        /// 打开后会记录连接、发送、接收、状态解析、站点映射和异常信息。
        /// </summary>
        [FieldMember] public bool EnableDetailLog = true;

        /// <summary>
        /// 是否在详细日志中输出原始 32 字节协议帧。
        /// 调试协议字段时建议打开；现场日志量过大时可关闭。
        /// </summary>
        [FieldMember] public bool LogRawFrame = true;

        /// <summary>
        /// 小车绘制长度，单位与场景坐标一致，默认 1200。
        /// 仅影响画面显示，不参与 AGV 协议通讯。
        /// </summary>
        [FieldMember] public float CarLength = 1200;

        /// <summary>
        /// 小车绘制宽度，单位与场景坐标一致，默认 800。
        /// 仅影响画面显示，不参与 AGV 协议通讯。
        /// </summary>
        [FieldMember] public float CarWidth = 800;

        /// <summary>
        /// 最近一次从 AGV 返回帧中解析出的完整状态快照。
        /// UI 显示、状态枚举和站点定位均以该对象为基础。
        /// </summary>
        public MagCarReport LastReport { get; private set; } = new MagCarReport();

        /// <summary>
        /// 创建 MagCar 默认实例，供 StandardScene 通过车型注册机制实例化。
        /// 默认地址为 127.0.0.1，实际现场地址应在车属性中配置。
        /// </summary>
        /// <returns>初始化后的 MagCar 实例。</returns>
        public new static async Task<MagCar> Create()
        {
            return new MagCar
            {
                lstatus = "连接中",
                address = "127.0.0.1",
                name = "MagCar",
                haveCoordination = true,
                speed = 1
            };
        }

        /// <summary>
        /// 在场景画布中绘制小车外形和朝向箭头。
        /// 绘制尺寸来自 <see cref="CarLength"/> 和 <see cref="CarWidth"/>。
        /// </summary>
        /// <param name="eGraphics">场景传入的绘图对象，坐标系已由基类转换到小车本地坐标。</param>
        protected override void draw(Graphics eGraphics)
        {
            var halfLength = CarLength / 2;
            var halfWidth = CarWidth / 2;
            eGraphics.FillRectangle(Brushes.Gray, -halfLength, -halfWidth, CarLength, CarWidth);
            eGraphics.DrawRectangle(Pens.White, -halfLength, -halfWidth, CarLength, CarWidth);

            using (var orientPen = new Pen(Color.White, 3))
            {
                orientPen.CustomEndCap = new AdjustableArrowCap(6, 6, true);
                orientPen.StartCap = LineCap.RoundAnchor;
                eGraphics.DrawLine(orientPen, 0, 0, halfLength, 0);
            }
        }

        /// <summary>
        /// 生成场景中小车悬浮或列表展示的简要状态文本。
        /// 内容包括车辆名称、站点、电量、速度和报警摘要。
        /// </summary>
        /// <returns>用于界面显示的多行状态字符串。</returns>
        public override string SetDisplayInfo()
        {
            try
            {
                var report = LastReport;
                var state = StateText(report.State);
                var alarm = report.Alarm == 0 ? "" : $"|alarm:0x{report.Alarm:X}";
                return $"{name}({id})\n{state}|site:{siteID}|soc:{report.Charge}|speed:{report.Speed}{alarm}";
            }
            catch
            {
                return $"{name}({id})\n状态未知";
            }
        }

        /// <summary>
        /// StandardScene 周期调用的保活入口。
        /// 方法内部按 <see cref="PollIntervalMs"/> 节流，实际执行时发送 0x00 查询状态命令，
        /// 并将返回的 Node、状态、电量、报警等字段同步到车对象和 status.enums。
        /// </summary>
        public override void keepAlive()
        {
            if ((DateTime.Now - _lastPollTime).TotalMilliseconds < PollIntervalMs)
            {
                return;
            }

            _lastPollTime = DateTime.Now;
            try
            {
                DetailLog($"keepAlive poll begin, interval={PollIntervalMs}ms, endpoint={address}:{Port}");
                var report = RefreshState();
                haveCoordination = true;
                DetailLog($"keepAlive poll ok, node={report.Node}, state={StateText(report.State)}, soc={report.Charge}, speed={report.Speed}, alarm=0x{report.Alarm:X}");
            }
            catch (Exception ex)
            {
                lstatus = IsBenignSocketFault(ex) ? lstatus : "离线";
                if (!IsBenignSocketFault(ex))
                {
                    DetailLog($"keepAlive poll failed: {ex.Message}");
                }
            }
        }

        private bool _running;

        /// <summary>
        /// 最近一次已同步到交通管控层的站点 id。
        /// keepAlive 仅更新 x/y 时，holdingLocks 不会自动跟随，需要显式同步。
        /// </summary>
        private int _lastSyncedTrafficSiteId = -1;

        /// <summary>
        /// 执行调度编译出的移动脚本。
        /// 脚本中的 <c>agv.MagCarGo</c> 会通过 FASS 1.0 协议下发移动任务，并轮询状态直到到达目标节点。
        /// </summary>
        /// <param name="script">调度层编译出的脚本文本。</param>
        public override async Task actualSendScript(string script)
        {
            if (_running)
            {
                throw new InvalidOperationException($"MagCar {id} already running script");
            }

            _running = true;
            try
            {
                DetailLog($"script begin, length={(script ?? string.Empty).Length}");
                var agv = new MagCarInterface(id);
                var tcs = new TaskCompletionSource<int>();
                new Thread(() =>
                {
                    try
                    {
                        SelfEvaluating(agv, script);
                        tcs.SetResult(1);
                    }
                    catch (Exception ex)
                    {
                        tcs.SetException(ex);
                    }
                })
                { Name = $"eva_{name}({id}):{status.programs.now.name}" }.Start();

                await tcs.Task;
                await agv.WaitAsync();
                DetailLog("script completed");
            }
            catch (Exception ex)
            {
                DetailLog($"script failed: {ExceptionFormatter.FormatEx(ex)}");
                throw;
            }
            finally
            {
                _running = false;
            }
        }

        [MethodMember(Name = "查询状态", Description = "按FASS 1.0协议查询AGV状态")]
        /// <summary>
        /// 人工查询 AGV 状态的界面方法。
        /// 调用后立即发送 0x00 查询命令，并用返回帧刷新 <see cref="LastReport"/>、站点位置和 status.enums。
        /// </summary>
        public void QueryState()
        {
            RefreshState();
        }

        [MethodMember(Name = "启动", Description = "发送启动指令")]
        /// <summary>
        /// 人工启动小车的界面方法。
        /// 按当前车头角度发送 0x01 启动命令。
        /// </summary>
        public void StartMagCar()
        {
            SendStart((ushort)Math.Round(th));
        }

        [MethodMember(Name = "停止", Description = "发送停止指令")]
        /// <summary>
        /// 人工停止小车的界面方法。
        /// 发送 0x02 停止命令，当前停止秒数传 0。
        /// </summary>
        public void StopMagCar()
        {
            SendStop(0);
        }

        [MethodMember(Name = "进入小车远程", Description = "进入小车远程桌面")]
        [I18N.DocumentTranslation(Name = "Open remote desktop", Description = "Open the car's remote desktop", locale = "en")]
        /// <summary>
        /// 打开 Windows 远程桌面客户端并连接到当前小车地址。
        /// 该方法只用于运维进入车载电脑，不参与 AGV 协议通讯。
        /// </summary>
        public void Mstsc()
        {
            Process.Start(
                new ProcessStartInfo
                {
                    FileName = "mstsc",
                    Arguments = $"/v:{address}",
                    UseShellExecute = false,
                    CreateNoWindow = true
                }
            );
        }

        /// <summary>
        /// 调度系统触发的急停入口。
        /// 记录急停原因后发送 0x02 停止命令。
        /// </summary>
        /// <param name="reason">触发急停的上层原因，写入诊断日志便于追查。</param>
        public void EmergencyStop(string reason)
        {
            Diagnosis.Post($"car{name}:MagCar EmergencyStop {reason}");
            SendStop(0);
        }

        /// <summary>
        /// 调度系统触发的急停释放入口。
        /// 记录释放日志后按当前车头角度发送 0x01 启动命令。
        /// </summary>
        public void EmergencyRelease()
        {
            Diagnosis.Post($"car{name}:MagCar EmergencyRelease");
            SendStart((ushort)Math.Round(th));
        }

        [MethodMember(Name = "重置TCP连接", Description = "关闭当前长连接，下次命令自动重连")]
        /// <summary>
        /// 人工重置长连接的界面方法。
        /// 关闭当前 TCP 长连接；下一次 keepAlive 或命令发送会自动重新连接。
        /// </summary>
        public void ResetTcpConnection()
        {
            lock (_syncRoot)
            {
                ClosePersistentConnection("manual reset");
            }
        }

        /// <summary>
        /// 发送 0x01 启动命令。
        /// </summary>
        /// <param name="angle">启动时同步给 AGV 的车头角度，按协议大端 ushort 写入数据区。</param>
        public void SendStart(ushort angle)
        {
            SendCommand(CmdStart, BuildUShortOrLegacyByte(angle));
        }

        /// <summary>
        /// 发送 0x02 停止命令。
        /// </summary>
        /// <param name="seconds">停止保持秒数；当前现场通常传 0 表示立即停止。</param>
        public void SendStop(ushort seconds)
        {
            SendCommand(CmdStop, BuildUShortOrLegacyByte(seconds));
        }

        /// <summary>
        /// 发送 0x03 速度设置命令。
        /// </summary>
        /// <param name="percent">速度百分比，推荐范围 0-100。</param>
        public void SendSpeedPercent(byte percent)
        {
            SendCommand(CmdSpeed, percent);
        }

        /// <summary>
        /// 发送 0x04 角度设置命令。
        /// </summary>
        /// <param name="angle">目标车头角度，单位度，按协议大端 ushort 写入数据区。</param>
        public void SendAngle(ushort angle)
        {
            SendCommand(CmdAngle, BuildUShortOrLegacyByte(angle));
        }

        /// <summary>
        /// 下发移动任务。
        /// 默认使用 0xA1 命令同时发送起点和终点；当 <see cref="UseTaskCommand"/> 为 false 时，
        /// 退回 0x05 旧版目标节点命令。
        /// </summary>
        /// <param name="fromNode">起点节点号，通常由当前站点或路径起点转换得到。</param>
        /// <param name="toNode">终点节点号，通常由目标站点转换得到。</param>
        public void SendMoveTask(ushort fromNode, ushort toNode)
        {
            if (UseTaskCommand)
            {
                DetailLog($"send move task by 0xA1, fromNode={fromNode}, toNode={toNode}");
                SendCommand(CmdTask, Combine(
                    GetBigEndian(fromNode),
                    GetBigEndian(toNode),
                    new[] { (byte)0x03 }));
                return;
            }

            DetailLog($"send legacy node by 0x05, toNode={toNode}");
            SendCommand(CmdLegacyNode, BuildUShortOrLegacyByte(toNode));
        }

        /// <summary>
        /// 主动查询并刷新 AGV 状态。
        /// 成功后会解析响应帧并调用 <see cref="ApplyReport(MagCarReport)"/> 更新车辆运行状态。
        /// </summary>
        /// <returns>本次查询解析出的状态报告。</returns>
        public MagCarReport RefreshState()
        {
            var response = SendCommand(CmdQueryState);
            var report = MagCarReport.Parse(response);
            ApplyReport(report);
            return report;
        }

        /// <summary>
        /// 构建并发送一条 FASS 1.0 命令帧。
        /// 该方法统一负责日志、线程加锁、发送接收、响应解析和状态同步。
        /// </summary>
        /// <param name="command">协议命令码。</param>
        /// <param name="data">需要写入数据区的协议字段，最多拷贝 26 字节。</param>
        /// <returns>AGV 返回的完整 32 字节响应帧。</returns>
        private byte[] SendCommand(byte command, params byte[] data)
        {
            var request = BuildMessage(command, data);
            DetailLog($"TX {CommandName(command)}, dataLen={(data == null ? 0 : data.Length)}");
            if (LogRawFrame)
            {
                DetailLog($"TX raw {ToHex(request)}");
            }

            byte[] response;
            lock (_syncRoot)
            {
                response = SendAndReceive(request);
            }

            DetailLog($"RX {CommandName(command)}, bytes={response.Length}");
            if (LogRawFrame)
            {
                DetailLog($"RX raw {ToHex(response)}");
            }

            if (response.Length >= 32)
            {
                ApplyReport(MagCarReport.Parse(response));
            }

            return response;
        }

        /// <summary>
        /// 根据连接模式发送请求并读取响应。
        /// 长连接和短连接都必须返回完整 32 字节响应帧。
        /// </summary>
        /// <param name="request">已构建好的 32 字节请求帧。</param>
        /// <returns>完整 32 字节响应帧。</returns>
        private byte[] SendAndReceive(byte[] request)
        {
            if (UsePersistentConnection)
            {
                return SendAndReceivePersistent(request);
            }

            return SendAndReceiveShort(request);
        }

        /// <summary>
        /// 使用长连接发送请求并读取响应。
        /// 发送或读取过程中出现异常时会关闭长连接，确保下一次命令重新建连。
        /// </summary>
        /// <param name="request">已构建好的 32 字节请求帧。</param>
        /// <returns>完整 32 字节响应帧。</returns>
        private byte[] SendAndReceivePersistent(byte[] request)
        {
            try
            {
                EnsurePersistentConnection();
                var stream = _persistentClient.GetStream();
                stream.Write(request, 0, request.Length);
                stream.Flush();
                return ReadExact(stream, 32);
            }
            catch (Exception ex)
            {
                ClosePersistentConnection($"persistent send/read failed: {ex.Message}");
                throw;
            }
        }

        /// <summary>
        /// 确保长连接客户端已经连接。
        /// 若当前连接为空或已断开，会先清理旧连接再按配置超时时间重新连接 AGV 服务端。
        /// </summary>
        private void EnsurePersistentConnection()
        {
            if (IsPersistentConnected())
            {
                return;
            }

            ClosePersistentConnection("prepare reconnect");
            DetailLog($"persistent connect begin {address}:{Port}, timeout={ConnectTimeoutMs}ms");
            var client = new TcpClient();
            try
            {
                ConnectWithTimeout(client);
                client.SendTimeout = SendTimeoutMs;
                client.ReceiveTimeout = ReceiveTimeoutMs;
                _persistentClient = client;
                DetailLog($"persistent connect ok {address}:{Port}");
            }
            catch
            {
                try
                {
                    client.Close();
                }
                catch
                {
                }

                _persistentClient = null;
                throw;
            }
        }

        /// <summary>
        /// 判断当前长连接对象是否存在且 TcpClient 仍处于 Connected 状态。
        /// 该检查只作为发送前的快速判断，真正断链仍可能在写入或读取时暴露。
        /// </summary>
        /// <returns>当前长连接可复用时返回 true。</returns>
        private bool IsPersistentConnected()
        {
            try
            {
                return _persistentClient != null && _persistentClient.Connected;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// 关闭并释放当前长连接对象。
        /// 该方法不会抛出关闭异常，保证故障恢复路径不会被二次异常打断。
        /// </summary>
        /// <param name="reason">关闭连接的原因，写入详细日志。</param>
        private void ClosePersistentConnection(string reason)
        {
            var client = _persistentClient;
            if (client == null)
            {
                return;
            }

            _persistentClient = null;
            DetailLog($"persistent connection closed, reason={reason}");
            try
            {
                if (client.Connected)
                {
                    client.Client?.Shutdown(SocketShutdown.Both);
                }
            }
            catch (Exception ex) when (IsBenignSocketFault(ex))
            {
            }
            catch
            {
            }

            try
            {
                client.Close();
            }
            catch (Exception ex) when (IsBenignSocketFault(ex))
            {
            }
            catch
            {
            }
        }

        /// <summary>
        /// 使用短连接兼容模式发送请求。
        /// 每条命令都会新建 TCP 连接、发送、读满响应后立即释放连接。
        /// </summary>
        /// <param name="request">已构建好的 32 字节请求帧。</param>
        /// <returns>完整 32 字节响应帧。</returns>
        private byte[] SendAndReceiveShort(byte[] request)
        {
            using (var client = new TcpClient())
            {
                DetailLog($"short connect begin {address}:{Port}, timeout={ConnectTimeoutMs}ms");
                ConnectWithTimeout(client);
                DetailLog($"short connect ok {address}:{Port}");

                client.SendTimeout = SendTimeoutMs;
                client.ReceiveTimeout = ReceiveTimeoutMs;

                using (var stream = client.GetStream())
                {
                    stream.Write(request, 0, request.Length);
                    stream.Flush();
                    return ReadExact(stream, 32);
                }
            }
        }

        /// <summary>
        /// 使用 <see cref="TcpClient.ConnectAsync(string, int, CancellationToken)"/> 实现带超时的 TCP 连接。
        /// 避免 BeginConnect 超时后直接 Close 触发“已中止 I/O 操作”的 SocketException。
        /// </summary>
        /// <param name="client">待连接的 TcpClient 实例。</param>
        private void ConnectWithTimeout(TcpClient client)
        {
            if (string.IsNullOrWhiteSpace(address))
            {
                throw new InvalidOperationException("MagCar address is empty");
            }

            using var cts = new CancellationTokenSource(ConnectTimeoutMs);
            try
            {
                client.ConnectAsync(address, Port, cts.Token).GetAwaiter().GetResult();
            }
            catch (OperationCanceledException) when (cts.IsCancellationRequested)
            {
                AbortClientQuietly(client);
                throw new TimeoutException($"connect {address}:{Port} timeout ({ConnectTimeoutMs}ms)");
            }
            catch (Exception ex) when (IsBenignSocketFault(ex) && cts.IsCancellationRequested)
            {
                AbortClientQuietly(client);
                throw new TimeoutException($"connect {address}:{Port} timeout ({ConnectTimeoutMs}ms)", ex);
            }
        }

        /// <summary>超时或取消建连时安静关闭客户端，吞掉预期的套接字中止异常。</summary>
        private static void AbortClientQuietly(TcpClient client)
        {
            if (client == null)
            {
                return;
            }

            try
            {
                client.Client?.Close();
            }
            catch (Exception ex) when (IsBenignSocketFault(ex))
            {
            }
            catch
            {
            }

            try
            {
                client.Close();
            }
            catch (Exception ex) when (IsBenignSocketFault(ex))
            {
            }
            catch
            {
            }
        }

        /// <summary>
        /// 判断是否为连接关闭/超时取消导致的预期套接字异常（无需打印完整堆栈）。
        /// </summary>
        private static bool IsBenignSocketFault(Exception ex)
        {
            for (var current = ex; current != null; current = current.InnerException)
            {
                if (current is ObjectDisposedException)
                {
                    return true;
                }

                if (current is SocketException socketEx)
                {
                    switch (socketEx.SocketErrorCode)
                    {
                        case SocketError.OperationAborted:
                        case SocketError.Interrupted:
                        case SocketError.ConnectionAborted:
                        case SocketError.ConnectionReset:
                        case SocketError.Shutdown:
                            return true;
                    }
                }

                if (current is IOException)
                {
                    var message = current.Message;
                    if (!string.IsNullOrEmpty(message) &&
                        (message.Contains("已中止 I/O", StringComparison.Ordinal) ||
                         message.Contains("I/O operation", StringComparison.OrdinalIgnoreCase) ||
                         message.Contains("operation was aborted", StringComparison.OrdinalIgnoreCase)))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// 从网络流中精确读取指定长度的数据。
        /// TCP 是字节流，不能假设一次 Read 就能拿到完整 32 字节协议包，因此这里循环读取直到读满。
        /// </summary>
        /// <param name="stream">已连接的网络流。</param>
        /// <param name="length">期望读取的字节数，MagCar 响应固定为 32。</param>
        /// <returns>长度等于 <paramref name="length"/> 的字节数组。</returns>
        private static byte[] ReadExact(NetworkStream stream, int length)
        {
            var buffer = new byte[length];
            var offset = 0;
            try
            {
                while (offset < length)
                {
                    var read = stream.Read(buffer, offset, length - offset);
                    if (read == 0)
                    {
                        throw new IOException("remote closed before full MagCar response");
                    }
                    offset += read;
                }
                return buffer;
            }
            catch (Exception ex) when (IsBenignSocketFault(ex))
            {
                throw new IOException("remote closed before full MagCar response", ex);
            }
        }

        /// <summary>
        /// 按 FASS 1.0 格式构造 32 字节发送帧。
        /// 帧结构为：起始字节、命令码、车辆编号、数据区、保留字节和结束字节。
        /// </summary>
        /// <param name="command">协议命令码。</param>
        /// <param name="data">数据区内容，最多写入第 4-29 字节。</param>
        /// <returns>完整 32 字节请求帧。</returns>
        private byte[] BuildMessage(byte command, byte[] data)
        {
            var bytes = new byte[32];
            bytes[0] = Begin;
            bytes[1] = command;
            var code = GetBigEndian(VehicleCode);
            bytes[2] = code[0];
            bytes[3] = code[1];

            if (data != null && data.Length > 0)
            {
                Array.Copy(data, 0, bytes, 4, Math.Min(data.Length, 26));
            }

            bytes[30] = 0x00;
            bytes[31] = End;
            return bytes;
        }

        /// <summary>
        /// 根据兼容开关构造数值字段。
        /// 标准文档模式返回 ushort 大端两字节；旧版兼容模式只返回低 1 字节。
        /// </summary>
        /// <param name="value">待写入协议数据区的数值。</param>
        /// <returns>符合当前兼容模式的字节数组。</returns>
        private byte[] BuildUShortOrLegacyByte(ushort value)
        {
            if (UseDocumentUShortFields)
            {
                return GetBigEndian(value);
            }

            return new[] { (byte)Clamp(value, 0, 255) };
        }

        /// <summary>
        /// 轮询 AGV 状态，直到车辆到达指定节点或超时。
        /// </summary>
        /// <param name="targetNode">目标节点号。</param>
        /// <param name="timeoutMs">超时时间，单位毫秒。</param>
        private void WaitUntilArrived(ushort targetNode, int timeoutMs)
        {
            var deadline = DateTime.Now.AddMilliseconds(timeoutMs <= 0 ? MoveTimeoutSeconds * 1000 : timeoutMs);
            while (DateTime.Now < deadline)
            {
                var report = RefreshState();
                if (report.Node == targetNode && report.State != 1)
                {
                    DetailLog($"arrived node={targetNode}");
                    return;
                }

                Thread.Sleep(Math.Max(50, Math.Min(PollIntervalMs, 500)));
            }

            throw new TimeoutException($"MagCar {name}({id}) move timeout, target node={targetNode}, current={LastReport.Node}");
        }

        /// <summary>
        /// 将 AGV 返回状态应用到车对象。
        /// 该方法会更新 <see cref="LastReport"/>、在线状态、车头角度、status.enums，
        /// 并把返回的 Node 映射为场景站点后同步 <see cref="Car.siteID"/>、坐标 x/y。
        /// </summary>
        /// <param name="report">已解析的 AGV 状态报告。</param>
        private void ApplyReport(MagCarReport report)
        {
            LastReport = report;
            lstatus = report.Alarm != 0 ? "报警" : report.State == 0 ? "未准备" : "上线";
            th = report.Angle;

            status.enums["Protocol"] = "FASS1.0";
            status.enums["CurrentNode"] = report.Node.ToString();
            status.enums["Alarm"] = $"0x{report.Alarm:X}";
            status.enums["State"] = StateText(report.State);
            status.enums["Soc"] = report.Charge.ToString();
            status.enums["ElectricCurrent"] = report.Current.ToString("0.###");
            status.enums["Voltage"] = report.Voltage.ToString("0.###");
            status.enums["Speed"] = report.Speed.ToString();
            status.enums["Angle"] = report.Angle.ToString();
            status.enums["Task"] = report.Task.ToString();
            status.enums["Lift"] = report.Lift.ToString();
            status.enums["Roll"] = report.Roll.ToString();

            var site = ResolveSite(report.Node);
            if (site == null)
            {
                haveCoordination = false;
                DetailLog($"report applied without mapped site, node={report.Node}, state={StateText(report.State)}, alarm=0x{report.Alarm:X}");
                return;
            }

            haveCoordination = true;
            siteID = site.id;
            x = site.x;
            y = site.y;
            SyncTrafficFromReport(site, report);
            DetailLog($"report applied, node={report.Node}, site={siteID}, x={x:0.###}, y={y:0.###}, th={th:0.###}, state={StateText(report.State)}, soc={report.Charge}, current={report.Current:0.###}, voltage={report.Voltage:0.###}, speed={report.Speed}, task={report.Task}, lift={report.Lift}, roll={report.Roll}, alarm=0x{report.Alarm:X}");
        }

        /// <summary>
        /// 将 AGV 上报的当前地标同步到交通锁（holdingLocks）。
        /// GhostCar 调度依赖 holdingLocks/GetLastSite，仅改 x/y/siteID 会导致“画面在动、初始化占点不动”。
        /// </summary>
        private void SyncTrafficFromReport(Site site, MagCarReport report)
        {
            if (_running || site == null)
            {
                return;
            }

            var lockedSite = status.holdingLocks.Length > 0 ? status.holdingLocks[0] : -1;
            if (lockedSite == site.id)
            {
                _lastSyncedTrafficSiteId = site.id;
                return;
            }

            if (_lastSyncedTrafficSiteId == site.id && lockedSite == site.id)
            {
                return;
            }

            try
            {
                DetailLog($"traffic sync TrafficReset, lock={lockedSite}, targetSite={site.id}, node={report.Node}, state={StateText(report.State)}");
                TrafficReset(site, true, strict: false);
                _lastSyncedTrafficSiteId = site.id;
            }
            catch (Exception ex)
            {
                DetailLog($"traffic sync failed: {ex.Message}");
            }
        }

        /// <summary>
        /// 将 AGV 返回的节点号解析为场景站点。
        /// 根据 <see cref="UseTagValueAsNode"/> 决定优先匹配站点 TagValue 还是直接匹配站点 id。
        /// </summary>
        /// <param name="node">AGV 协议返回的当前节点号。</param>
        /// <returns>匹配到的站点；未匹配或节点为 0 时返回 null。</returns>
        private Site ResolveSite(ushort node)
        {
            if (node == 0)
            {
                return null;
            }

            if (UseTagValueAsNode)
            {
                var byTagValue = SimpleLib.GetAllSites().FirstOrDefault(site =>
                    site.fields.TryGetValue("TagValue", out var tag) &&
                    int.TryParse(tag, out var tagValue) &&
                    tagValue == node);
                if (byTagValue != null)
                {
                    return byTagValue;
                }
            }

            return SimpleLib.GetAllSites().FirstOrDefault(site => site.id == node);
        }

        /// <summary>
        /// 将场景站点 id 转换为 AGV 节点号。
        /// 当 <see cref="UseTagValueAsNode"/> 为 true 且站点配置了可解析的 TagValue 时使用 TagValue，
        /// 否则直接使用站点 id 并限制在 ushort 范围内。
        /// </summary>
        /// <param name="siteId">场景站点 id。</param>
        /// <returns>下发给 AGV 的节点号。</returns>
        private ushort ResolveNodeId(int siteId)
        {
            var site = SimpleLib.GetSite(siteId);
            if (UseTagValueAsNode &&
                site.fields.TryGetValue("TagValue", out var tag) &&
                ushort.TryParse(tag, out var tagValue))
            {
                return tagValue;
            }

            return (ushort)Clamp(siteId, 0, ushort.MaxValue);
        }

        /// <summary>
        /// 将路径或车辆配置中的速度换算为协议需要的百分比。
        /// 输入小于等于 1 时按比例值处理；大于 1 时按已经是百分比处理。
        /// </summary>
        /// <param name="trackSpeed">路径速度配置；小于等于 0 时回退到车辆自身 speed 字段。</param>
        /// <returns>0-100 范围内的速度百分比。</returns>
        private byte SpeedToPercent(double trackSpeed)
        {
            if (trackSpeed <= 0)
            {
                return (byte)Clamp((int)Math.Round(speed * 100), 0, 100);
            }

            if (trackSpeed <= 1)
            {
                return (byte)Clamp((int)Math.Round(trackSpeed * 100), 0, 100);
            }

            return (byte)Clamp((int)Math.Round(trackSpeed), 0, 100);
        }

        /// <summary>
        /// 根据起点和终点坐标计算车头方向角。
        /// 支持反向行驶、角度偏置和绝对角度覆盖。
        /// </summary>
        /// <param name="srcX">起点 X 坐标。</param>
        /// <param name="srcY">起点 Y 坐标。</param>
        /// <param name="dstX">终点 X 坐标。</param>
        /// <param name="dstY">终点 Y 坐标。</param>
        /// <param name="reverse">是否按反向行驶计算，true 时角度增加 180 度。</param>
        /// <param name="bias">额外角度偏置，单位度。</param>
        /// <param name="enableAbs">是否直接使用绝对角度。</param>
        /// <param name="absDir">启用绝对角度时使用的方向角。</param>
        /// <returns>归一化到 0-359 的方向角。</returns>
        private static ushort DirectionAngle(double srcX, double srcY, double dstX, double dstY, bool reverse, double bias, bool enableAbs, double absDir)
        {
            var angle = enableAbs ? absDir : Math.Atan2(dstY - srcY, dstX - srcX) / Math.PI * 180.0;
            if (reverse)
            {
                angle += 180;
            }
            angle += bias;
            angle %= 360;
            if (angle < 0)
            {
                angle += 360;
            }
            return (ushort)Math.Round(angle);
        }

        /// <summary>
        /// 将 ushort 转换为大端字节序。
        /// FASS 1.0 协议中的车辆编号、节点号和角度字段均按大端写入。
        /// </summary>
        /// <param name="value">待转换的 ushort 值。</param>
        /// <returns>长度为 2 的大端字节数组。</returns>
        private static byte[] GetBigEndian(ushort value)
        {
            var bytes = BitConverter.GetBytes(value);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(bytes);
            }
            return bytes;
        }

        /// <summary>
        /// 顺序拼接多个字节数组。
        /// 主要用于组装 0xA1 任务命令的数据区。
        /// </summary>
        /// <param name="arrays">需要按顺序拼接的字节数组集合。</param>
        /// <returns>拼接后的新字节数组。</returns>
        private static byte[] Combine(params byte[][] arrays)
        {
            var result = new byte[arrays.Sum(array => array.Length)];
            var offset = 0;
            foreach (var array in arrays)
            {
                Buffer.BlockCopy(array, 0, result, offset, array.Length);
                offset += array.Length;
            }
            return result;
        }

        /// <summary>
        /// 将整数限制在指定闭区间内。
        /// </summary>
        /// <param name="value">原始值。</param>
        /// <param name="min">允许的最小值。</param>
        /// <param name="max">允许的最大值。</param>
        /// <returns>限制后的值。</returns>
        private static int Clamp(int value, int min, int max)
        {
            if (value < min)
            {
                return min;
            }
            return value > max ? max : value;
        }

        /// <summary>
        /// 将 ushort 值限制在指定闭区间内。
        /// </summary>
        /// <param name="value">原始 ushort 值。</param>
        /// <param name="min">允许的最小值。</param>
        /// <param name="max">允许的最大值。</param>
        /// <returns>限制后的值。</returns>
        private static int Clamp(ushort value, int min, int max)
        {
            return Clamp((int)value, min, max);
        }

        /// <summary>
        /// 将协议状态字节转换为便于日志和界面阅读的中文文本。
        /// </summary>
        /// <param name="state">AGV 返回帧第 14 字节的状态码。</param>
        /// <returns>状态文本；未知状态会带出原始状态码。</returns>
        private static string StateText(byte state)
        {
            switch (state)
            {
                case 0x00:
                    return "未准备";
                case 0x01:
                    return "运行中";
                case 0x02:
                    return "停止中";
                case 0x03:
                    return "充电中";
                default:
                    return $"未知({state})";
            }
        }

        /// <summary>
        /// 输出 MagCar 专用详细日志。
        /// 日志同时写入车辆调试信息和 Diagnosis 通道，便于现场排查通讯问题。
        /// </summary>
        /// <param name="message">需要记录的日志正文。</param>
        private void DetailLog(string message)
        {
            if (!EnableDetailLog)
            {
                return;
            }

            var text = $"[MagCar:{name}({id})] {DateTime.Now:HH:mm:ss.fff} {message}";
            AppendDebug(text);
            Diagnosis.Post(text, "MagCar", true);
        }

        /// <summary>
        /// 将协议命令码转换为带十六进制值的命令名称。
        /// </summary>
        /// <param name="command">协议命令码。</param>
        /// <returns>用于日志输出的命令名称。</returns>
        private static string CommandName(byte command)
        {
            switch (command)
            {
                case CmdQueryState:
                    return "QueryState(0x00)";
                case CmdStart:
                    return "Start(0x01)";
                case CmdStop:
                    return "Stop(0x02)";
                case CmdSpeed:
                    return "Speed(0x03)";
                case CmdAngle:
                    return "Angle(0x04)";
                case CmdLegacyNode:
                    return "LegacyNode(0x05)";
                case CmdTask:
                    return "Task(0xA1)";
                default:
                    return $"Unknown(0x{command:X2})";
            }
        }

        /// <summary>
        /// 将字节数组格式化为空格分隔的十六进制字符串。
        /// 用于输出原始协议帧日志。
        /// </summary>
        /// <param name="bytes">待格式化的字节数组。</param>
        /// <returns>十六进制字符串；输入为 null 时返回空字符串。</returns>
        private static string ToHex(byte[] bytes)
        {
            return bytes == null ? string.Empty : BitConverter.ToString(bytes).Replace("-", " ");
        }

        /// <summary>
        /// MagCar 调度脚本执行接口，将路径段转换为 FASS 1.0 移动任务。
        /// </summary>
        internal sealed class MagCarInterface : AGVInterface
        {
            private readonly MagCar _car;

            public MagCarInterface(int id)
            {
                _car = (MagCar)SimpleLib.GetCar(id);
            }

            public override bool TryLock(int siteId)
            {
                if (!_car.status.usage.Get().scheduling)
                {
                    throw new Exception("abandoned");
                }

                return TrafficControl.TryLock(_car, siteId);
            }

            public override void Leave(int siteId)
            {
                TrafficControl.Leave(_car, siteId);
            }

            public void MagCarGo(int srcId, int dstId, double speed = -1)
            {
                var promise = new TaskCompletionSource<int>();
                Queue(async () =>
                {
                    while (!TryLock(dstId))
                    {
                        await Task.Delay(50);
                    }

                    var fromNode = _car.ResolveNodeId(srcId);
                    var toNode = _car.ResolveNodeId(dstId);
                    _car.DetailLog($"MagCarGo src={srcId}, dst={dstId}, fromNode={fromNode}, toNode={toNode}, speed={speed}");

                    if (_car.StartBeforeMove)
                    {
                        _car.SendStart((ushort)Math.Round(_car.th));
                    }

                    _car.SendSpeedPercent(_car.SpeedToPercent(speed));
                    _car.SendMoveTask(fromNode, toNode);
                    _car.WaitUntilArrived(toNode, _car.MoveTimeoutSeconds * 1000);
                    promise.SetResult(1);
                }, async () =>
                {
                    await promise.Task;
                    Leave(srcId);
                });
            }
        }

        /// <summary>
        /// AGV 返回的 32 字节状态帧解析结果。
        /// 字段位置遵循 FASS 1.0 协议：Node 在第 4-5 字节，Alarm 在第 6-13 字节，
        /// State 在第 14 字节，Charge 在第 15 字节，Current/Voltage 为大端 float。
        /// </summary>
        public class MagCarReport
        {
            /// <summary>
            /// 响应帧中的命令码，通常与请求命令码一致，用于确认本次返回对应的命令类型。
            /// </summary>
            public byte Command { get; private set; }

            /// <summary>
            /// 响应帧中的车辆编号，来自第 2-3 字节，按大端 ushort 解析。
            /// </summary>
            public ushort Code { get; private set; }

            /// <summary>
            /// AGV 当前所在节点号，来自第 4-5 字节。
            /// <see cref="ApplyReport(MagCarReport)"/> 会将该值映射到场景站点并更新车辆坐标。
            /// </summary>
            public ushort Node { get; private set; }

            /// <summary>
            /// AGV 报警位图，来自第 6-13 字节。
            /// 值为 0 表示无报警，非 0 时车辆在线状态会显示为报警。
            /// </summary>
            public ulong Alarm { get; private set; }

            /// <summary>
            /// AGV 运行状态码，来自第 14 字节。
            /// 当前按 0 未准备、1 运行中、2 停止中、3 充电中展示。
            /// </summary>
            public byte State { get; private set; }

            /// <summary>
            /// 电量百分比，来自第 15 字节，也会写入 status.enums["Soc"]。
            /// </summary>
            public byte Charge { get; private set; }

            /// <summary>
            /// 当前电流，来自第 16-19 字节，按大端 IEEE754 float 解析。
            /// </summary>
            public float Current { get; private set; }

            /// <summary>
            /// 当前电压，来自第 20-23 字节，按大端 IEEE754 float 解析。
            /// </summary>
            public float Voltage { get; private set; }

            /// <summary>
            /// AGV 当前速度百分比或协议返回速度值，来自第 24 字节。
            /// </summary>
            public byte Speed { get; private set; }

            /// <summary>
            /// AGV 当前车头角度，来自第 25-26 字节，按大端 ushort 解析。
            /// 该值会同步到车辆 th 字段。
            /// </summary>
            public ushort Angle { get; private set; }

            /// <summary>
            /// 当前任务状态或任务编号，来自第 27 字节。
            /// </summary>
            public byte Task { get; private set; }

            /// <summary>
            /// 顶升机构状态，来自第 28 字节。
            /// </summary>
            public byte Lift { get; private set; }

            /// <summary>
            /// 滚筒机构状态，来自第 29 字节。
            /// </summary>
            public byte Roll { get; private set; }

            /// <summary>
            /// 将 AGV 返回的原始 32 字节帧解析为 <see cref="MagCarReport"/>。
            /// 方法会校验帧长、起始字节和结束字节，字段按 FASS 1.0 大端格式读取。
            /// </summary>
            /// <param name="bytes">AGV 返回的原始响应帧。</param>
            /// <returns>解析后的状态报告。</returns>
            public static MagCarReport Parse(byte[] bytes)
            {
                if (bytes == null || bytes.Length < 32)
                {
                    throw new ArgumentException("MagCar response must be 32 bytes");
                }
                if (bytes[0] != Begin || bytes[31] != End)
                {
                    throw new ArgumentException($"invalid MagCar frame: begin=0x{bytes[0]:X2}, end=0x{bytes[31]:X2}");
                }

                return new MagCarReport
                {
                    Command = bytes[1],
                    Code = ReadUInt16(bytes, 2),
                    Node = ReadUInt16(bytes, 4),
                    Alarm = ReadUInt64(bytes, 6),
                    State = bytes[14],
                    Charge = bytes[15],
                    Current = ReadSingle(bytes, 16),
                    Voltage = ReadSingle(bytes, 20),
                    Speed = bytes[24],
                    Angle = ReadUInt16(bytes, 25),
                    Task = bytes[27],
                    Lift = bytes[28],
                    Roll = bytes[29]
                };
            }

            /// <summary>
            /// 从指定偏移读取大端 ushort。
            /// </summary>
            /// <param name="bytes">来源字节数组。</param>
            /// <param name="offset">字段起始偏移。</param>
            /// <returns>解析出的 ushort 值。</returns>
            private static ushort ReadUInt16(byte[] bytes, int offset)
            {
                return (ushort)((bytes[offset] << 8) | bytes[offset + 1]);
            }

            /// <summary>
            /// 从指定偏移读取大端 ulong。
            /// 报警位图占用 8 字节，因此使用该方法解析 Alarm 字段。
            /// </summary>
            /// <param name="bytes">来源字节数组。</param>
            /// <param name="offset">字段起始偏移。</param>
            /// <returns>解析出的 ulong 值。</returns>
            private static ulong ReadUInt64(byte[] bytes, int offset)
            {
                var result = 0UL;
                for (var i = 0; i < 8; i++)
                {
                    result = (result << 8) | bytes[offset + i];
                }
                return result;
            }

            /// <summary>
            /// 从指定偏移读取大端单精度浮点数。
            /// 当前用于解析电流和电压字段。
            /// </summary>
            /// <param name="bytes">来源字节数组。</param>
            /// <param name="offset">字段起始偏移。</param>
            /// <returns>解析出的 float 值。</returns>
            private static float ReadSingle(byte[] bytes, int offset)
            {
                var buffer = new byte[4];
                Buffer.BlockCopy(bytes, offset, buffer, 0, 4);
                if (BitConverter.IsLittleEndian)
                {
                    Array.Reverse(buffer);
                }
                return BitConverter.ToSingle(buffer, 0);
            }
        }
    }
}
