#nullable enable
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using IGP.UnitySDK.Models;
using IGP.UnitySDK.Network;
using IGP.UnitySDK.Protocol;
using IGP.UnitySDK.ThirdParty.Kcp2k;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace IGP.UnitySDK.Core
{
    /// <summary>
    /// Unity 版本 KCP 客户端（lowlevel IGP.UnitySDK.ThirdParty.Kcp2k.Kcp + UDP Socket）。
    ///
    /// 注意：本实现使用 length-prefixed JSON：
    /// [len:4 bytes big-endian] + [json bytes]
    /// 并通过 WS 获取 token 后在 KCP 上发送 kcp_handshake。
    /// </summary>
    public class IGPKcpClient : IDisposable
    {
        private const string LogScope = "kcp";
        private const int HeaderLength = 4;
        private const int MaxDatagramSize = 1500;
        private const float StateLogIntervalSeconds = 30f;
        private const float BusyStateLogIntervalSeconds = 30f;
        private const float TransportRateSampleIntervalSeconds = 0.5f;
        private const int QueueWarningThreshold = 256;
        private const int QueueCriticalThreshold = 1024;
        private const int SendQueueBackpressureThreshold = 1024;
        private const int SendQueueHardFaultThreshold = 2048;
        private const int ReceiveHardDatagramLimit = 2048;
        private const double ReceiveExtraBudgetSeconds = 0.0015;
        private const double SuspectAfterSeconds = 3.0;
        public const int DefaultMaxDatagramsPerTick = 256;
        public const int DefaultWindowSize = 256;
        public const int MinWindowSize = 128;
        public const int MaxWindowSize = 1024;

        private readonly object stateLock = new object();
        private readonly IGPNetworkAnomalyLogLimiter anomalyLogLimiter = new IGPNetworkAnomalyLogLimiter();
        private IGPReliableTransportOptions transportOptions = IGPReliableTransportOptions.Default;
        private IGPKcpFrameDecoder frameDecoder;
        private Socket? socket;
        private EndPoint? remoteEndPoint;
        private Kcp? kcp;
        private uint conversationId;
        private bool isDisposed = false;

        private readonly byte[] datagramBuffer = new byte[MaxDatagramSize];
        private byte[] kcpReceiveBuffer;

        private bool authenticated;
        private uint nextUpdate;

        // 心跳 + RTT 相关字段
        private IGPRTTStats rttStats = new IGPRTTStats();
        private Dictionary<int, long> pendingPings = new Dictionary<int, long>();
        private int pingSeq;
        private float lastPongTime;
        private float lastHeartbeatTime;
        private bool isAlive;
        private float heartbeatInterval = 1.0f;
        private bool heartbeatRunning;
        private bool handshakeDatagramLogged;
        private string handshakeTarget = string.Empty;
        private float connectedAt;
        private float lastStateLogAt;
        private int lastStateLogQueueTotal;
        private int totalDatagramsReceived;
        private int totalDatagramsSent;
        private long totalDatagramBytesReceived;
        private long totalDatagramBytesSent;
        private int totalFramesSent;
        private int totalFramesReceived;
        private int totalPayloadsReceived;
        private int totalReceiveErrors;
        private int totalSendErrors;
        private int totalDroppedSends;
        private long totalOutputAttempts;
        private long totalSocketAccepted;
        private long totalSocketWouldBlock;
        private long totalSocketFatal;
        private long totalKcpInputErrors;
        private bool hasTransportRateSample;
        private float lastTransportRateSampleTime;
        private int lastRateDatagramsReceived;
        private int lastRateDatagramsSent;
        private long lastRateDatagramBytesReceived;
        private long lastRateDatagramBytesSent;
        private float datagramsReceivedPerSecond;
        private float datagramsSentPerSecond;
        private float datagramBytesReceivedPerSecond;
        private float datagramBytesSentPerSecond;
        private int maxDatagramsPerTick = DefaultMaxDatagramsPerTick;
        private int sendWindowSize = DefaultWindowSize;
        private int receiveWindowSize = DefaultWindowSize;
        private IGPKcpHealthState healthState = IGPKcpHealthState.Disconnected;
        private IGPKcpFaultReason lastFaultReason = IGPKcpFaultReason.None;
        private IGPKcpFaultReason pendingFaultReason = IGPKcpFaultReason.None;
        private bool faultDispatched;
        private bool acceptingBusinessSends;
        private double lastProtocolProgressAt;
        private uint observedSndUna;
        private uint observedRcvNxt;
        private float observedLastPongTime;

        public string? RoomId { get; private set; }
        public string? PlayerId { get; private set; }
        public bool IsConnected => socket != null && kcp != null && authenticated;
        public IGPLogLevel LogLevel { get; set; }
        public bool DebugLogging
        {
            get => LogLevel != IGPLogLevel.Off;
            set => LogLevel = value ? IGPLogLevel.Debug : IGPLogLevel.Off;
        }
        public string DiagnosticsSummary => BuildDiagnosticsSummary();
        public IGPKcpHealthState HealthState => healthState;
        public IGPKcpFaultReason LastFaultReason => lastFaultReason;

        /// <summary>
        /// 每次 Tick 最多读取的 UDP datagram 数，用于控制突发包的单帧处理预算。
        /// </summary>
        public int MaxDatagramsPerTick
        {
            get => maxDatagramsPerTick;
            set => maxDatagramsPerTick = Math.Max(1, value);
        }

        public int SendWindowSize
        {
            get => sendWindowSize;
            set
            {
                sendWindowSize = NormalizeWindowSize(value);
                ApplyWindowSize();
            }
        }

        public int ReceiveWindowSize
        {
            get => receiveWindowSize;
            set
            {
                receiveWindowSize = NormalizeWindowSize(value);
                ApplyWindowSize();
            }
        }
        
        /// <summary>
        /// Compatibility alias for app-level heartbeat ping/pong RTT stats.
        /// </summary>
        public IGPRTTStats RTTStats => rttStats;

        /// <summary>
        /// App-level heartbeat ping/pong RTT stats.
        /// </summary>
        public IGPRTTStats AppRTTStats => rttStats;

        /// <summary>
        /// Snapshot of KCP ACK timing and queue state.
        /// </summary>
        public IGPKcpTransportStats TransportStats => BuildTransportStats();
        
        /// <summary>
        /// 连接是否活跃（基于心跳）
        /// </summary>
        public bool IsAlive => isAlive && IsConnected &&
            (Time.realtimeSinceStartupAsDouble - lastPongTime) < heartbeatInterval * 3;
        
        /// <summary>
        /// 心跳间隔（秒）
        /// </summary>
        public float HeartbeatInterval
        {
            get => heartbeatInterval;
            set => heartbeatInterval = value;
        }

        public event Action<Message>? MessageReceived;
        public event Action<string>? ErrorOccurred;
        public event Action<bool>? ConnectionStateChanged;
        public event Action<IGPKcpFaultReason>? TransportFaulted;

        public IGPKcpClient()
        {
            frameDecoder = new IGPKcpFrameDecoder(transportOptions);
            kcpReceiveBuffer = new byte[transportOptions.KcpFrameMaxBytes + HeaderLength];
        }

        internal void ApplyTransportOptions(IGPReliableTransportOptions options)
        {
            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            lock (stateLock)
            {
                transportOptions = options;
                frameDecoder = new IGPKcpFrameDecoder(transportOptions);
                kcpReceiveBuffer = new byte[transportOptions.KcpFrameMaxBytes + HeaderLength];
            }

            LogLifecycle(
                "options-applied",
                $"reliableMaxBytes={options.ReliableMessageMaxBytes} reliableChunkBytes={options.ReliableChunkMaxBytes} " +
                $"kcpPayloadMaxBytes={options.KcpDataPlanePayloadMaxBytes} kcpFrameMaxBytes={options.KcpFrameMaxBytes}");
        }

        public void Connect(string host, int port, string roomId, string playerId, string token)
        {
            if (socket != null && kcp != null)
            {
                if (ShouldLog(IGPLogLevel.Info))
                {
                    LogLifecycle("connect", $"event=skipped reason=already-created {BuildDiagnosticsSummary()}");
                }

                return;
            }

            Disconnect();

            RoomId = roomId;
            PlayerId = playerId;
            authenticated = false;
            frameDecoder.Reset();
            
            // 初始化心跳和RTT
            rttStats = new IGPRTTStats();
            pendingPings = new Dictionary<int, long>();
            pingSeq = 0;
            lastPongTime = Time.realtimeSinceStartup;
            lastHeartbeatTime = Time.realtimeSinceStartup;
            isAlive = true;
            heartbeatRunning = false;
            handshakeDatagramLogged = false;
            handshakeTarget = string.Empty;
            connectedAt = 0f;
            lastStateLogAt = 0f;
            lastStateLogQueueTotal = 0;
            totalDatagramsReceived = 0;
            totalDatagramsSent = 0;
            totalDatagramBytesReceived = 0;
            totalDatagramBytesSent = 0;
            totalFramesSent = 0;
            totalFramesReceived = 0;
            totalPayloadsReceived = 0;
            totalReceiveErrors = 0;
            totalSendErrors = 0;
            totalDroppedSends = 0;
            totalOutputAttempts = 0;
            totalSocketAccepted = 0;
            totalSocketWouldBlock = 0;
            totalSocketFatal = 0;
            totalKcpInputErrors = 0;
            healthState = IGPKcpHealthState.Handshaking;
            lastFaultReason = IGPKcpFaultReason.None;
            pendingFaultReason = IGPKcpFaultReason.None;
            faultDispatched = false;
            acceptingBusinessSends = true;
            lastProtocolProgressAt = Time.realtimeSinceStartupAsDouble;
            observedSndUna = 0;
            observedRcvNxt = 0;
            observedLastPongTime = lastPongTime;
            ResetTransportRates();

            try
            {
                var ip = ResolveIP(host);
                remoteEndPoint = new IPEndPoint(ip, port);
                handshakeTarget = remoteEndPoint.ToString() ?? $"{host}:{port}";
                LogLifecycle(
                    "connect",
                    $"event=start targetHost={host} resolvedIP={ip} port={port} endpoint={handshakeTarget} " +
                    $"roomId={roomId} localPlayerId={playerId}");

                socket = new Socket(ip.AddressFamily, SocketType.Dgram, ProtocolType.Udp)
                {
                    Blocking = false
                };
                socket.Connect(remoteEndPoint);

                uint conv = CreateRandomConv();
                conversationId = conv;
                kcp = new Kcp(conv, (buffer, size) =>
                {
                    totalOutputAttempts += 1;
                    try
                    {
                        var sent = socket?.Send(buffer, 0, size, SocketFlags.None) ?? 0;
                        totalDatagramsSent += sent > 0 ? 1 : 0;
                        totalDatagramBytesSent += sent > 0 ? sent : 0;
                        totalSocketAccepted += sent > 0 ? 1 : 0;
                        if (!authenticated && !handshakeDatagramLogged)
                        {
                            handshakeDatagramLogged = true;
                            if (ShouldLog(IGPLogLevel.Info))
                            {
                                LogLifecycle(
                                    "handshake",
                                    $"event=datagram-sent endpoint={handshakeTarget} bytes={sent} " +
                                    $"roomId={roomId} localPlayerId={playerId} " +
                                    BuildKcpStateSummary());
                            }
                        }
                    }
                    catch (SocketException ex) when (IsWouldBlock(ex.SocketErrorCode))
                    {
                        totalSocketWouldBlock += 1;
                        totalSendErrors += 1;
                        if (ShouldLog(IGPLogLevel.Warning))
                        {
                            anomalyLogLimiter.ObserveEvent(
                                "udp-send-would-block",
                                Time.realtimeSinceStartup,
                                () => $"socketError={ex.SocketErrorCode} total={totalSocketWouldBlock} {BuildKcpStateSummary()}",
                                message => LogWarning("anomaly", message));
                        }
                    }
                    catch (SocketException ex)
                    {
                        totalSocketFatal += 1;
                        totalSendErrors += 1;
                        if (!QueueFault(IGPKcpFaultReason.SocketFailure))
                        {
                            return;
                        }
                        if (ShouldLog(IGPLogLevel.Warning))
                        {
                            LogWarning("send", $"event=udp-send-failed bytes={size} socketError={ex.SocketErrorCode} error={ex.Message} {BuildKcpStateSummary()}");
                        }

                        ErrorOccurred?.Invoke($"KCP output send failed: {ex.Message}");
                    }
                    catch (Exception ex)
                    {
                        totalSocketFatal += 1;
                        totalSendErrors += 1;
                        if (QueueFault(IGPKcpFaultReason.SocketFailure))
                        {
                            ErrorOccurred?.Invoke($"KCP output send failed: {ex.Message}");
                        }
                    }
                });

                kcp.SetNoDelay(1, 10, 2, nocwnd: true);
                kcp.SetMtu(1200);
                ApplyWindowSize();
                kcp.SetInterval(10);

                var now = NowMs();
                nextUpdate = now;

                // kcp handshake (framed)
                var handshake = new
                {
                    type = "kcp_handshake",
                    token = token
                };
                if (ShouldLog(IGPLogLevel.Info))
                {
                    LogLifecycle(
                        "handshake",
                        $"event=queued endpoint={handshakeTarget} roomId={roomId} localPlayerId={playerId} token={FormatTokenForLog(token)} " +
                        $"config={BuildTransportOptionsSummary()} {BuildKcpStateSummary()}");
                }

                SendRawJson(handshake);
                LogStateIfNeeded(force: true, reason: "connect-started");
            }
            catch (Exception ex)
            {
                totalSendErrors += 1;
                if (ShouldLog(IGPLogLevel.Error))
                {
                    LogError(
                        "connect",
                        $"event=connect-failed targetHost={host} port={port} roomId={roomId} localPlayerId={playerId} " +
                        $"error={ex.Message} {BuildKcpStateSummary()}");
                }

                ErrorOccurred?.Invoke($"KCP connect failed: {ex.Message}");
                Disconnect();
            }
        }

        public void Disconnect()
        {
            DisconnectInternal(suppressDisposedGuard: false);
        }

        private void DisconnectInternal(bool suppressDisposedGuard)
        {
            string summary;
            bool hadState;
            lock (stateLock)
            {
                if (isDisposed && !suppressDisposedGuard) return;

                hadState = socket != null ||
                           kcp != null ||
                           authenticated ||
                           heartbeatRunning ||
                           !string.IsNullOrEmpty(RoomId) ||
                           !string.IsNullOrEmpty(PlayerId) ||
                           totalDatagramsReceived > 0 ||
                           totalDatagramsSent > 0 ||
                           totalFramesReceived > 0 ||
                           totalFramesSent > 0;
                summary = ShouldLog(IGPLogLevel.Info) ? BuildDiagnosticsSummary() : string.Empty;
                authenticated = false;
                heartbeatRunning = false;
                acceptingBusinessSends = false;
                healthState = IGPKcpHealthState.Disconnected;
                RoomId = null;
                PlayerId = null;
                frameDecoder.Reset();
                pendingPings?.Clear();
                handshakeDatagramLogged = false;
                handshakeTarget = string.Empty;

                try
                {
                    socket?.Close(0);
                }
                catch
                {
                    // ignore
                }

                socket = null;
                remoteEndPoint = null;
                kcp = null;
                conversationId = 0;
            }

            if (hadState && ShouldLog(IGPLogLevel.Info))
            {
                LogLifecycle("disconnect", $"event=complete {summary}");
            }

            ConnectionStateChanged?.Invoke(false);
        }

        public void Dispose()
        {
            lock (stateLock)
            {
                if (isDisposed) return;
                isDisposed = true;
            }

            DisconnectInternal(suppressDisposedGuard: true);
        }

        /// <summary>
        /// 由 IGPRuntimeManager 在 Update() 中驱动。
        /// </summary>
        public void Tick()
        {
            if (socket == null || kcp == null)
            {
                healthState = IGPKcpHealthState.Disconnected;
                return;
            }

            uint now = NowMs();
            int datagramsThisTick = 0;
            int datagramBytesThisTick = 0;
            int kcpInputErrorsThisTick = 0;
            bool receiveBudgetExhausted = false;
            double receiveStartedAt = Time.realtimeSinceStartupAsDouble;

            try
            {
                int softBudget = Math.Min(MaxDatagramsPerTick, ReceiveHardDatagramLimit);
                double extraBudgetStartedAt = 0;
                bool extraBudgetStarted = false;
                while (socket.Poll(0, SelectMode.SelectRead))
                {
                    if (datagramsThisTick >= ReceiveHardDatagramLimit)
                    {
                        receiveBudgetExhausted = true;
                        break;
                    }
                    if (datagramsThisTick >= softBudget)
                    {
                        if (!extraBudgetStarted)
                        {
                            extraBudgetStartedAt = Time.realtimeSinceStartupAsDouble;
                            extraBudgetStarted = true;
                        }
                        else if (Time.realtimeSinceStartupAsDouble - extraBudgetStartedAt >= ReceiveExtraBudgetSeconds)
                        {
                            receiveBudgetExhausted = true;
                            break;
                        }
                    }

                    int received = socket.Receive(datagramBuffer, 0, datagramBuffer.Length, SocketFlags.None);
                    if (received > 0)
                    {
                        datagramsThisTick += 1;
                        datagramBytesThisTick += received;
                        totalDatagramsReceived += 1;
                        totalDatagramBytesReceived += received;
                        int inputResult = kcp.Input(datagramBuffer, 0, received);
                        if (inputResult != 0)
                        {
                            kcpInputErrorsThisTick += 1;
                            totalKcpInputErrors += 1;
                            totalReceiveErrors += 1;
                            if (ShouldLog(IGPLogLevel.Warning))
                            {
                                LogWarning(
                                    "recv",
                                    $"event=kcp-input-failed result={inputResult} datagramBytes={received} " +
                                    $"{BuildKcpStateSummary()}");
                            }
                        }

                    }
                    else
                    {
                        break;
                    }
                }

                if (!receiveBudgetExhausted && socket.Poll(0, SelectMode.SelectRead))
                {
                    receiveBudgetExhausted = true;
                }
                if (receiveBudgetExhausted)
                {
                    if (ShouldLog(IGPLogLevel.Warning))
                    {
                        anomalyLogLimiter.ObserveEvent(
                            "udp-read-budget-exhausted",
                            Time.realtimeSinceStartup,
                            () => $"datagrams={datagramsThisTick} elapsedMs={(Time.realtimeSinceStartupAsDouble - receiveStartedAt) * 1000.0:F2} " +
                                  $"softLimit={softBudget} hardLimit={ReceiveHardDatagramLimit} socketStillReadable=true {BuildKcpStateSummary()}",
                            message => LogWarning("anomaly", message));
                    }
                }
            }
            catch (SocketException ex) when (IsWouldBlock(ex.SocketErrorCode))
            {
                totalSocketWouldBlock += 1;
                totalReceiveErrors += 1;
                if (ShouldLog(IGPLogLevel.Warning))
                {
                    anomalyLogLimiter.ObserveEvent(
                        "udp-receive-would-block",
                        Time.realtimeSinceStartup,
                        () => $"socketError={ex.SocketErrorCode} total={totalSocketWouldBlock} {BuildKcpStateSummary()}",
                        message => LogWarning("anomaly", message));
                }
            }
            catch (SocketException ex)
            {
                totalSocketFatal += 1;
                totalReceiveErrors += 1;
                if (QueueFault(IGPKcpFaultReason.SocketFailure) && ShouldLog(IGPLogLevel.Warning))
                {
                    LogWarning("recv", $"event=udp-receive-failed socketError={ex.SocketErrorCode} error={ex.Message} {BuildKcpStateSummary()}");
                }
            }
            catch (Exception ex)
            {
                totalReceiveErrors += 1;
                if (ShouldLog(IGPLogLevel.Warning))
                {
                    LogWarning("recv", $"event=udp-receive-failed error={ex.Message} {BuildKcpStateSummary()}");
                }

                ErrorOccurred?.Invoke($"KCP receive failed: {ex.Message}");
            }

            if (datagramsThisTick > 0 && ShouldLog(IGPLogLevel.Debug))
            {
                LogDebug(
                    "recv",
                    $"event=udp-read tickDatagrams={datagramsThisTick} tickBytes={datagramBytesThisTick} " +
                    $"inputErrors={kcpInputErrorsThisTick} {BuildKcpStateSummary()}");
            }

            if (UpdateTransportRates())
            {
                EvaluateNetworkAnomalyWarnings();
            }

            try
            {
                if (now >= nextUpdate)
                {
                    kcp.Update(now);
                    nextUpdate = kcp.Check(now);
                }
                DrainKcpReceive();
                if (datagramsThisTick > 0 && now < nextUpdate)
                {
                    FlushKcpOutput("recv-between-updates");
                }
                LogStateIfNeeded(force: false, reason: "post-update");
                
                // 自动心跳逻辑
                if (heartbeatRunning && authenticated)
                {
                    float currentTime = Time.realtimeSinceStartup;
                    if (currentTime - lastHeartbeatTime >= heartbeatInterval)
                    {
                        SendPing();
                        lastHeartbeatTime = currentTime;
                    }
                }
            }
            catch (Exception ex)
            {
                totalReceiveErrors += 1;
                if (ShouldLog(IGPLogLevel.Error))
                {
                    LogError("tick", $"event=failed error={ex.Message} {BuildKcpStateSummary()}");
                }

                ErrorOccurred?.Invoke($"KCP tick failed: {ex.Message}");
            }

            EvaluateTransportHealth(receiveBudgetExhausted);
            DispatchPendingFault();
        }

        private void DrainKcpReceive()
        {
            if (kcp == null) return;

            int payloads = 0;
            int payloadBytes = 0;
            int frameCount = 0;
            while (true)
            {
                int n = kcp.Receive(kcpReceiveBuffer, kcpReceiveBuffer.Length);
                if (n <= 0)
                {
                    break;
                }

                var frames = frameDecoder.Append(kcpReceiveBuffer, 0, n);
                payloads += 1;
                payloadBytes += n;
                frameCount += frames.Count;
                foreach (var frame in frames)
                {
                    totalFramesReceived += 1;
                    HandlePayload(frame);
                }
            }

            if (payloads > 0)
            {
                totalPayloadsReceived += payloads;
                if (ShouldLog(IGPLogLevel.Debug))
                {
                    LogDebug(
                        "recv",
                        $"event=kcp-receive-drained payloads={payloads} payloadBytes={payloadBytes} " +
                        $"frames={frameCount} {BuildKcpStateSummary()}");
                }
            }
        }

        public void SendMessage(Message message)
        {
            _ = TrySendMessage(message);
        }

        public bool TrySendMessage(Message message)
        {
            if (!IsConnected || !authenticated || !acceptingBusinessSends ||
                string.IsNullOrEmpty(RoomId) || string.IsNullOrEmpty(PlayerId))
            {
                totalDroppedSends += 1;
                if (ShouldLog(IGPLogLevel.Warning))
                {
                    LogWarning(
                        "send",
                        $"event=send-dropped reason=not-ready connected={IsConnected} authenticated={authenticated} " +
                        $"roomId={RoomId ?? string.Empty} localPlayerId={PlayerId ?? string.Empty} type={message?.type ?? string.Empty} " +
                        BuildKcpStateSummary());
                }

                return false;
            }

            if (!CanAcceptBusinessSend())
            {
                totalDroppedSends += 1;
                return false;
            }

            message.roomId ??= RoomId;
            message.playerId ??= PlayerId;

            if (TryEncodeBinaryDataPlaneMessage(message, out var binaryPayload))
            {
                return SendFrame(binaryPayload, $"type={message.type} encoding=binary");
            }

            var json = JsonConvert.SerializeObject(message);
            return SendFrame(Encoding.UTF8.GetBytes(json), $"type={message.type} encoding=json");
        }

        /// <summary>
        /// 启动自动心跳（同时测量RTT）
        /// </summary>
        /// <param name="interval">心跳间隔（秒），默认1秒</param>
        public void StartHeartbeat(float interval = 1.0f)
        {
            heartbeatInterval = interval;
            heartbeatRunning = true;
            lastHeartbeatTime = Time.realtimeSinceStartup;
            if (ShouldLog(IGPLogLevel.Info))
            {
                LogLifecycle("heartbeat", $"event=started intervalSeconds={interval:F2} {BuildKcpStateSummary()}");
            }
        }
        
        /// <summary>
        /// 停止自动心跳
        /// </summary>
        public void StopHeartbeat()
        {
            heartbeatRunning = false;
            if (ShouldLog(IGPLogLevel.Info))
            {
                LogLifecycle("heartbeat", $"event=stopped {BuildKcpStateSummary()}");
            }
        }
        
        /// <summary>
        /// 发送Ping（手动或由心跳自动调用）
        /// </summary>
        public void SendPing()
        {
            if (!IsConnected || !authenticated)
            {
                if (ShouldLog(IGPLogLevel.Debug))
                {
                    LogDebug(
                        "heartbeat",
                        $"event=heartbeat-skipped reason=not-ready connected={IsConnected} authenticated={authenticated} " +
                        BuildKcpStateSummary());
                }

                return;
            }

            if (!CanAcceptBusinessSend())
            {
                return;
            }
            
            long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            int seq = pingSeq++;
            
            pendingPings[seq] = timestamp;
            if (ShouldLog(IGPLogLevel.Debug))
            {
                LogDebug(
                    "heartbeat",
                    $"event=ping-sent seq={seq} queuedAtUnixMs={timestamp} pendingPings={pendingPings.Count} " +
                    BuildKcpStateSummary());
            }
            
            SendPing(timestamp, seq);
        }
        
        /// <summary>
        /// 发送Ping（内部方法）
        /// </summary>
        private void SendPing(long clientTimestamp, int seq)
        {
            if (!IsConnected) return;

            var ping = new Message
            {
                type = "ping",
                roomId = RoomId ?? string.Empty,
                playerId = PlayerId ?? string.Empty,
                reliable = true,
                content = new Dictionary<string, object>
                {
                    ["clientTimestamp"] = clientTimestamp,
                    ["seq"] = seq,
                }
            };
            _ = TrySendMessage(ping);
        }

        private void SendRawJson(object obj)
        {
            var json = JsonConvert.SerializeObject(obj);
            _ = SendFrame(
                Encoding.UTF8.GetBytes(json),
                $"type={obj.GetType().Name} encoding=json-raw",
                applyBackpressure: false);
        }

        private bool SendFrame(byte[] payload, string detail, bool applyBackpressure = true)
        {
            if (kcp == null)
            {
                totalDroppedSends += 1;
                LogWarning("send", $"event=send-dropped reason=no-kcp {detail}");
                return false;
            }

            if (applyBackpressure && !CanAcceptBusinessSend())
            {
                totalDroppedSends += 1;
                return false;
            }

            var frame = IGPKcpFrameCodec.EncodeFrame(payload, transportOptions);
            int result = kcp.Send(frame, 0, frame.Length);
            if (result == 0)
            {
                totalFramesSent += 1;
                FlushKcpOutput("send");
                if (ShouldLog(IGPLogLevel.Debug))
                {
                    LogDebug(
                        "send",
                        $"event=send-queued {detail} payloadBytes={payload.Length} frameBytes={frame.Length} " +
                        $"result={result} {BuildKcpStateSummary()}");
                }

                LogStateIfNeeded(force: false, reason: "send");
            }
            else
            {
                totalDroppedSends += 1;
                if (ShouldLog(IGPLogLevel.Warning))
                {
                    LogWarning(
                        "send",
                        $"event=send-rejected {detail} payloadBytes={payload.Length} frameBytes={frame.Length} " +
                        $"result={result} {BuildKcpStateSummary()}");
                }
            }

            return result == 0;
        }

        private void FlushKcpOutput(string reason)
        {
            if (kcp == null)
            {
                return;
            }

            try
            {
                uint now = NowMs();
                kcp.Update(now);
                kcp.Flush();
                nextUpdate = kcp.Check(now);
            }
            catch (Exception ex)
            {
                totalSendErrors += 1;
                if (ShouldLog(IGPLogLevel.Warning))
                {
                    LogWarning(
                        "send",
                        $"event=flush-failed reason={reason} error={ex.Message} {BuildKcpStateSummary()}");
                }

                ErrorOccurred?.Invoke($"KCP flush failed: {ex.Message}");
            }
        }

        private void ApplyWindowSize()
        {
            if (kcp == null)
            {
                return;
            }

            kcp.SetWindowSize((uint)sendWindowSize, (uint)receiveWindowSize);
        }

        private void HandlePayload(byte[] payload)
        {
            if (TryHandleBinaryEnvelope(payload))
            {
                return;
            }

            var json = Encoding.UTF8.GetString(payload);
            if (string.IsNullOrWhiteSpace(json))
            {
                return;
            }

            try
            {
                var obj = JObject.Parse(json);
                var t = obj.Value<string>("type");
                
                if (t == "kcp_handshake_ack")
                {
                    if (!authenticated)
                    {
                        authenticated = true;
                        connectedAt = Time.realtimeSinceStartup;
                        acceptingBusinessSends = true;
                        lastProtocolProgressAt = Time.realtimeSinceStartupAsDouble;
                        observedSndUna = kcp?.snd_una ?? 0;
                        observedRcvNxt = kcp?.rcv_nxt ?? 0;
                        TransitionHealth(IGPKcpHealthState.Healthy, "handshake-ack");
                        if (ShouldLog(IGPLogLevel.Info))
                        {
                            LogLifecycle("handshake", $"event=ack {BuildKcpStateSummary()}");
                        }

                        LogStateIfNeeded(force: true, reason: "handshake-ack");
                        ConnectionStateChanged?.Invoke(true);
                    }
                }
                else if (t == "pong")
                {
                    // 处理pong响应，计算RTT
                    HandlePong(obj);
                }
                else
                {
                    if (ShouldLog(IGPLogLevel.Debug))
                    {
                        LogDebug(
                            "recv",
                            $"event=json-received type={t ?? "<missing>"} bytes={payload.Length} {BuildKcpStateSummary()}");
                    }
                }
            }
            catch
            {
                // ignore type check errors
            }

            try
            {
                var message = JsonConvert.DeserializeObject<Message>(json);
                if (message != null)
                {
                    MessageReceived?.Invoke(message);
                }
            }
            catch (Exception ex)
            {
                totalReceiveErrors += 1;
                if (ShouldLog(IGPLogLevel.Warning))
                {
                    LogWarning("recv", $"event=payload-parse-failed error={ex.Message} bytes={payload.Length} {BuildKcpStateSummary()}");
                }

                ErrorOccurred?.Invoke($"Failed to parse KCP message: {ex.Message}");
            }
        }

        private bool TryEncodeBinaryDataPlaneMessage(Message message, out byte[] payload)
        {
            payload = Array.Empty<byte>();
            if (!string.Equals(message.type, "p2p_data", StringComparison.Ordinal) || message.content == null)
            {
                return false;
            }

            P2PMessagePayload? dataPayload;
            if (message.content is P2PMessagePayload typedPayload)
            {
                dataPayload = typedPayload;
            }
            else if (message.content is JObject jobj)
            {
                dataPayload = jobj.ToObject<P2PMessagePayload>();
            }
            else
            {
                dataPayload = JsonConvert.DeserializeObject<P2PMessagePayload>(
                    JsonConvert.SerializeObject(message.content));
            }

            if (dataPayload == null ||
                string.IsNullOrWhiteSpace(dataPayload.data))
            {
                return false;
            }

            bool hasReliableChunkMetadata = HasReliableChunkMetadata(dataPayload);
            string transportChannel = IGPP2PTransportChannels.Resolve(
                dataPayload.transportChannel,
                hasReliableChunkMetadata);
            if (dataPayload.transportSequence.HasValue ||
                !string.Equals(transportChannel, IGPP2PTransportChannels.Data, StringComparison.Ordinal))
            {
                return false;
            }

            byte[] rawPayload = Convert.FromBase64String(dataPayload.data);
            string senderPlayerId = !string.IsNullOrWhiteSpace(dataPayload.senderId)
                ? dataPayload.senderId
                : message.playerId ?? PlayerId ?? string.Empty;
            string targetPlayerId = dataPayload.targetId ?? message.targetPlayerId ?? string.Empty;
            var targetKind = string.IsNullOrWhiteSpace(targetPlayerId)
                ? IGPKcpTargetKind.Broadcast
                : IGPKcpTargetKind.Player;

            IGPKcpBinaryEnvelope envelope;
            if (hasReliableChunkMetadata)
            {
                if (!HasCompleteReliableChunkMetadata(dataPayload))
                {
                    return false;
                }

                envelope = new IGPKcpBinaryEnvelope(
                    version: IGPKcpBinaryEnvelopeCodec.Version2,
                    flags: IGPKcpBinaryEnvelopeCodec.ReliableChunkFlag,
                    messageType: dataPayload.messageType,
                    targetKind: targetKind,
                    senderPlayerId: senderPlayerId,
                    targetPlayerId: targetPlayerId,
                    payload: rawPayload,
                    reliableMessageId: dataPayload.reliableMessageId,
                    reliableChunkIndex: dataPayload.reliableChunkIndex,
                    reliableChunkCount: dataPayload.reliableChunkCount,
                    reliableTotalBytes: dataPayload.reliableTotalBytes,
                    reliableMessageType: dataPayload.reliableMessageType);
            }
            else
            {
                envelope = new IGPKcpBinaryEnvelope(
                    version: IGPKcpBinaryEnvelopeCodec.Version1,
                    flags: 0,
                    messageType: dataPayload.messageType,
                    targetKind: targetKind,
                    senderPlayerId: senderPlayerId,
                    targetPlayerId: targetPlayerId,
                    payload: rawPayload);
            }

            try
            {
                payload = IGPKcpBinaryEnvelopeCodec.Encode(envelope, transportOptions);
                return true;
            }
            catch (Exception ex)
            {
                if (ShouldLog(IGPLogLevel.Debug))
                {
                    LogDebug(
                        "send",
                        $"event=binary-envelope-skipped reason=encode-failed reliableChunk={hasReliableChunkMetadata} " +
                        $"error={ex.Message} {BuildKcpStateSummary()}");
                }

                payload = Array.Empty<byte>();
                return false;
            }
        }

        private bool TryHandleBinaryEnvelope(byte[] payload)
        {
            if (payload == null ||
                payload.Length == 0 ||
                (payload[0] != IGPKcpBinaryEnvelopeCodec.Version1 &&
                 payload[0] != IGPKcpBinaryEnvelopeCodec.Version2))
            {
                return false;
            }

            try
            {
                var envelope = IGPKcpBinaryEnvelopeCodec.Decode(payload);
                var p2pPayload = new P2PMessagePayload
                {
                    senderId = envelope.SenderPlayerId,
                    targetId = envelope.TargetPlayerId,
                    data = Convert.ToBase64String(envelope.Payload),
                    messageType = envelope.MessageType,
                    transportChannel = IGPP2PTransportChannels.Data,
                    reliable = true
                };
                if (envelope.IsReliableChunk)
                {
                    p2pPayload.reliableMessageId = envelope.ReliableMessageId;
                    p2pPayload.reliableChunkIndex = envelope.ReliableChunkIndex;
                    p2pPayload.reliableChunkCount = envelope.ReliableChunkCount;
                    p2pPayload.reliableTotalBytes = envelope.ReliableTotalBytes;
                    p2pPayload.reliableMessageType = envelope.ReliableMessageType;
                }

                MessageReceived?.Invoke(new Message
                {
                    type = "p2p_data",
                    roomId = RoomId ?? string.Empty,
                    playerId = envelope.SenderPlayerId,
                    targetPlayerId = envelope.TargetPlayerId,
                    reliable = true,
                    content = p2pPayload
                });
                if (ShouldLog(IGPLogLevel.Debug))
                {
                    LogDebug(
                        "recv",
                        $"event=binary-envelope-received type={envelope.MessageType} sourcePlayerId={envelope.SenderPlayerId} " +
                        $"targetPlayerId={envelope.TargetPlayerId} channel={IGPP2PTransportChannels.Data} " +
                        $"version={envelope.Version} reliableChunk={envelope.IsReliableChunk} bytes={envelope.Payload.Length} " +
                        $"{FormatReliableChunkLog(envelope)} {BuildKcpStateSummary()}");
                }

                return true;
            }
            catch (Exception ex)
            {
                totalReceiveErrors += 1;
                if (ShouldLog(IGPLogLevel.Warning))
                {
                    LogWarning("recv", $"event=binary-envelope-parse-failed error={ex.Message} bytes={payload.Length} {BuildKcpStateSummary()}");
                }

                ErrorOccurred?.Invoke($"Failed to parse KCP binary envelope: {ex.Message}");
                return true;
            }
        }

        private static bool HasReliableChunkMetadata(P2PMessagePayload dataPayload)
        {
            return !string.IsNullOrWhiteSpace(dataPayload.reliableMessageId) ||
                   dataPayload.reliableChunkIndex.HasValue ||
                   dataPayload.reliableChunkCount.HasValue ||
                   dataPayload.reliableTotalBytes.HasValue ||
                   dataPayload.reliableMessageType.HasValue;
        }

        private static bool HasCompleteReliableChunkMetadata(P2PMessagePayload dataPayload)
        {
            return !string.IsNullOrWhiteSpace(dataPayload.reliableMessageId) &&
                   dataPayload.reliableChunkIndex.HasValue &&
                   dataPayload.reliableChunkCount.HasValue &&
                   dataPayload.reliableTotalBytes.HasValue &&
                   dataPayload.reliableMessageType.HasValue;
        }

        private static string FormatReliableChunkLog(IGPKcpBinaryEnvelope envelope)
        {
            if (!envelope.IsReliableChunk)
            {
                return "chunk=-";
            }

            return
                $"messageId={envelope.ReliableMessageId} " +
                $"chunkIndex={envelope.ReliableChunkIndex} chunkCount={envelope.ReliableChunkCount} " +
                $"totalBytes={envelope.ReliableTotalBytes} reliableMessageType={envelope.ReliableMessageType}";
        }
        
        /// <summary>
        /// 处理Pong响应，计算RTT
        /// </summary>
        private void HandlePong(JObject pongMessage)
        {
            try
            {
                var content = pongMessage["content"] as JObject;
                if (content == null || content.Count == 0)
                {
                    return;
                }
                
                // 从content中获取seq和clientTimestamp
                var seqToken = content["seq"];
                var timestampToken = content["clientTimestamp"];
                
                if (seqToken == null || timestampToken == null)
                {
                    return;
                }
                
                int seq = seqToken.Type == JTokenType.Float 
                    ? (int)seqToken.Value<double>() 
                    : seqToken.Value<int>();
                    
                if (!pendingPings.TryGetValue(seq, out var localClientTimestamp))
                {
                    return;
                }
                
                pendingPings.Remove(seq);
                
                // 计算RTT（使用本地记录的客户端时间戳，避免被响应内容污染）
                long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                float rtt = (now - localClientTimestamp) / 1000f; // 转换为秒
                rttStats.AddSample(rtt);
                if (ShouldLog(IGPLogLevel.Debug))
                {
                    LogDebug(
                        "heartbeat",
                        $"event=heartbeat-pong seq={seq} queuedAtUnixMs={localClientTimestamp} " +
                        $"pongRecvAtUnixMs={now} rttMs={rtt * 1000f:F1} " +
                        $"avgRttMs={rttStats.AvgRTT * 1000f:F1} pendingPings={pendingPings.Count} " +
                        BuildKcpStateSummary());
                }
                
                // 更新心跳状态（保活）
                lastPongTime = Time.realtimeSinceStartup;
                isAlive = true;
            }
            catch (Exception ex)
            {
                totalReceiveErrors += 1;
                if (ShouldLog(IGPLogLevel.Warning))
                {
                    LogWarning("heartbeat", $"event=pong-parse-failed error={ex.Message} {BuildKcpStateSummary()}");
                }
            }
        }

        private bool CanAcceptBusinessSend()
        {
            if (kcp == null || !acceptingBusinessSends)
            {
                return false;
            }

            if (kcp.WaitSnd >= SendQueueHardFaultThreshold)
            {
                QueueFault(IGPKcpFaultReason.SendQueueOverflow);
                return false;
            }

            if (kcp.WaitSnd < SendQueueBackpressureThreshold)
            {
                return true;
            }

            if (ShouldLog(IGPLogLevel.Warning))
            {
                anomalyLogLimiter.ObserveEvent(
                    "kcp-send-backpressure",
                    Time.realtimeSinceStartup,
                    () => $"waitSnd={kcp.WaitSnd} threshold={SendQueueBackpressureThreshold} droppedSends={totalDroppedSends} {BuildKcpStateSummary()}",
                    message => LogWarning("anomaly", message));
            }
            return false;
        }

        private void EvaluateTransportHealth(bool receiveBudgetExhausted)
        {
            if (kcp == null)
            {
                TransitionHealth(IGPKcpHealthState.Disconnected, "no-kcp");
                return;
            }

            if (kcp.state < 0)
            {
                QueueFault(IGPKcpFaultReason.KcpDeadLink);
                return;
            }
            if (kcp.WaitSnd >= SendQueueHardFaultThreshold)
            {
                QueueFault(IGPKcpFaultReason.SendQueueOverflow);
                return;
            }

            double now = Time.realtimeSinceStartupAsDouble;
            bool progressed = kcp.snd_una != observedSndUna ||
                              kcp.rcv_nxt != observedRcvNxt ||
                              lastPongTime != observedLastPongTime;
            if (progressed)
            {
                observedSndUna = kcp.snd_una;
                observedRcvNxt = kcp.rcv_nxt;
                observedLastPongTime = lastPongTime;
                lastProtocolProgressAt = now;
                if (receiveBudgetExhausted)
                {
                    TransitionHealth(IGPKcpHealthState.Suspect, "receive-budget-exhausted");
                    return;
                }
                if (authenticated && pendingFaultReason == IGPKcpFaultReason.None)
                {
                    TransitionHealth(IGPKcpHealthState.Healthy, "protocol-progress");
                }
                return;
            }

            if (!authenticated)
            {
                TransitionHealth(IGPKcpHealthState.Handshaking, "awaiting-handshake");
                return;
            }

            bool heartbeatTimedOut = heartbeatRunning && !IsAlive;
            bool pendingWithoutProgress = kcp.WaitSnd > 0;
            double stalledFor = Math.Max(0, now - lastProtocolProgressAt);
            if (receiveBudgetExhausted || heartbeatTimedOut ||
                (pendingWithoutProgress && stalledFor >= SuspectAfterSeconds))
            {
                TransitionHealth(IGPKcpHealthState.Suspect, receiveBudgetExhausted
                    ? "receive-budget-exhausted"
                    : "protocol-progress-delayed");
                return;
            }

            TransitionHealth(IGPKcpHealthState.Healthy, "within-progress-window");
        }

        private bool QueueFault(IGPKcpFaultReason reason)
        {
            if (reason == IGPKcpFaultReason.None || faultDispatched || pendingFaultReason != IGPKcpFaultReason.None)
            {
                return false;
            }

            pendingFaultReason = reason;
            acceptingBusinessSends = false;
            TransitionHealth(IGPKcpHealthState.Stalled, $"fault-{reason}");
            return true;
        }

        private void DispatchPendingFault()
        {
            if (pendingFaultReason == IGPKcpFaultReason.None || faultDispatched)
            {
                return;
            }

            faultDispatched = true;
            lastFaultReason = pendingFaultReason;
            if (ShouldLog(IGPLogLevel.Error))
            {
                LogError(
                    "health",
                    $"event=transport-fault reason={lastFaultReason} stalledForMs={Math.Max(0, (Time.realtimeSinceStartupAsDouble - lastProtocolProgressAt) * 1000):F0} " +
                    BuildKcpStateSummary());
            }
            TransportFaulted?.Invoke(lastFaultReason);
        }

        private void TransitionHealth(IGPKcpHealthState next, string reason)
        {
            if (healthState == next)
            {
                return;
            }

            IGPKcpHealthState previous = healthState;
            healthState = next;
            if (ShouldLog(IGPLogLevel.Info))
            {
                LogLifecycle(
                    "health",
                    $"event=transition previous={previous} current={next} reason={reason} " +
                    $"lastProgressAgeMs={Math.Max(0, (Time.realtimeSinceStartupAsDouble - lastProtocolProgressAt) * 1000):F0} {BuildKcpStateSummary()}");
            }
        }

        private static bool IsWouldBlock(SocketError error) =>
            error == SocketError.WouldBlock || error == SocketError.IOPending;

        private uint NowMs()
        {
            return (uint)(Time.realtimeSinceStartupAsDouble * 1000.0);
        }

        private static IPAddress ResolveIP(string host)
        {
            if (IPAddress.TryParse(host, out var ip))
            {
                return ip;
            }

            var addresses = Dns.GetHostAddresses(host);
            if (addresses.Length == 0)
            {
                throw new InvalidOperationException($"Cannot resolve host: {host}");
            }

            foreach (var addr in addresses)
            {
                if (addr.AddressFamily == AddressFamily.InterNetwork)
                {
                    return addr;
                }
            }

            return addresses[0];
        }

        private static uint CreateRandomConv()
        {
            Span<byte> b = stackalloc byte[4];
            RandomNumberGenerator.Fill(b);
            return BinaryPrimitives.ReadUInt32LittleEndian(b);
        }

        private static string FormatTokenForLog(string token)
        {
            if (string.IsNullOrWhiteSpace(token))
            {
                return "<empty>";
            }

            if (token.Length <= 8)
            {
                return token;
            }

            return $"{token[..4]}...{token[^4..]}";
        }

        private void LogDebug(string path, string message)
        {
            IGPLog.Debug(LogScope, path, message);
        }

        private void LogLifecycle(string path, string message)
        {
            IGPLog.Info(LogScope, path, message);
        }

        private void LogWarning(string path, string message)
        {
            IGPLog.Warning(LogScope, path, message);
        }

        private void LogError(string path, string message)
        {
            IGPLog.Error(LogScope, path, message);
        }

        private bool ShouldLog(IGPLogLevel messageLevel)
        {
            return IGPLog.ShouldLog(messageLevel);
        }

        private void LogStateIfNeeded(bool force, string reason)
        {
            float now = Time.realtimeSinceStartup;
            int queueTotal = GetKcpQueueTotal();
            bool busy = queueTotal >= QueueWarningThreshold;
            bool critical = queueTotal >= QueueCriticalThreshold;
            IGPLogLevel stateLogLevel = busy || critical ? IGPLogLevel.Warning : IGPLogLevel.Info;
            if (!ShouldLog(stateLogLevel))
            {
                return;
            }

            float interval = busy ? BusyStateLogIntervalSeconds : StateLogIntervalSeconds;

            if (!force &&
                now - lastStateLogAt < interval &&
                Math.Abs(queueTotal - lastStateLogQueueTotal) < QueueWarningThreshold)
            {
                return;
            }

            lastStateLogAt = now;
            lastStateLogQueueTotal = queueTotal;

            string message = $"event=state reason={reason} {BuildDiagnosticsSummary()}";
            if (critical)
            {
                LogWarning("state", $"level=critical action=observe-only {message}");
            }
            else if (busy)
            {
                LogWarning("state", $"level=busy action=observe-only {message}");
            }
            else
            {
                LogLifecycle("state", message);
            }
        }

        private int GetKcpQueueTotal()
        {
            if (kcp == null)
            {
                return 0;
            }

            return kcp.snd_queue.Count +
                   kcp.snd_buf.Count +
                   kcp.rcv_queue.Count +
                   kcp.rcv_buf.Count;
        }

        private string BuildDiagnosticsSummary()
        {
            string endpoint = remoteEndPoint?.ToString() ?? handshakeTarget;
            string uptime = connectedAt > 0f
                ? $"{Math.Max(0f, Time.realtimeSinceStartup - connectedAt):F1}s"
                : "0.0s";

            return
                $"connected={IsConnected} authenticated={authenticated} alive={IsAlive} health={healthState} fault={lastFaultReason} heartbeatRunning={heartbeatRunning} " +
                $"heartbeatIntervalMs={heartbeatInterval * 1000f:F0} pendingPings={pendingPings.Count} " +
                $"maxDatagramsPerTick={MaxDatagramsPerTick} sendWindow={SendWindowSize} receiveWindow={ReceiveWindowSize} " +
                $"roomId={RoomId ?? string.Empty} localPlayerId={PlayerId ?? string.Empty} endpoint={endpoint} uptime={uptime} " +
                $"{BuildTransportOptionsSummary()} {BuildRttSummary()} {BuildKcpStateSummary()} " +
                $"totals(datagramsIn={totalDatagramsReceived},datagramsOut={totalDatagramsSent}," +
                $"datagramBytesIn={totalDatagramBytesReceived},datagramBytesOut={totalDatagramBytesSent},framesIn={totalFramesReceived}," +
                $"framesOut={totalFramesSent},payloadsIn={totalPayloadsReceived},sendErrors={totalSendErrors}," +
                $"receiveErrors={totalReceiveErrors},droppedSends={totalDroppedSends},outputAttempted={totalOutputAttempts}," +
                $"socketAccepted={totalSocketAccepted},socketWouldBlock={totalSocketWouldBlock},socketFatal={totalSocketFatal}," +
                $"kcpInputErrors={totalKcpInputErrors})";
        }

        private string BuildTransportOptionsSummary()
        {
            return
                $"limits(payloadMax={transportOptions.KcpDataPlanePayloadMaxBytes},frameMax={transportOptions.KcpFrameMaxBytes}," +
                $"reliableMax={transportOptions.ReliableMessageMaxBytes},chunkMax={transportOptions.ReliableChunkMaxBytes})";
        }

        private string BuildRttSummary()
        {
            if (rttStats.SampleCount == 0)
            {
                return "appRtt(samples=0)";
            }

            return
                $"appRtt(samples={rttStats.SampleCount},lastMs={rttStats.LastRTT * 1000f:F1}," +
                $"avgMs={rttStats.AvgRTT * 1000f:F1},minMs={rttStats.MinRTT * 1000f:F1}," +
                $"maxMs={rttStats.MaxRTT * 1000f:F1})";
        }

        private IGPKcpTransportStats BuildTransportStats()
        {
            if (kcp == null)
            {
                return IGPKcpTransportStats.Unavailable;
            }

            return new IGPKcpTransportStats(
                isAvailable: true,
                smoothedRttMs: kcp.rx_srtt,
                rtoMs: kcp.rx_rto,
                rttVarMs: kcp.rx_rttval,
                waitSnd: kcp.WaitSnd,
                sndQueue: kcp.snd_queue.Count,
                sndBuf: kcp.snd_buf.Count,
                rcvQueue: kcp.rcv_queue.Count,
                rcvBuf: kcp.rcv_buf.Count,
                ackList: kcp.acklist.Count,
                cwnd: kcp.cwnd,
                remoteWindow: kcp.rmt_wnd,
                datagramsIn: totalDatagramsReceived,
                datagramsOut: totalDatagramsSent,
                datagramsInPerSecond: datagramsReceivedPerSecond,
                datagramsOutPerSecond: datagramsSentPerSecond,
                datagramBytesIn: totalDatagramBytesReceived,
                datagramBytesOut: totalDatagramBytesSent);
        }

        private void ResetTransportRates()
        {
            hasTransportRateSample = false;
            lastTransportRateSampleTime = 0f;
            lastRateDatagramsReceived = totalDatagramsReceived;
            lastRateDatagramsSent = totalDatagramsSent;
            lastRateDatagramBytesReceived = totalDatagramBytesReceived;
            lastRateDatagramBytesSent = totalDatagramBytesSent;
            datagramsReceivedPerSecond = 0f;
            datagramsSentPerSecond = 0f;
            datagramBytesReceivedPerSecond = 0f;
            datagramBytesSentPerSecond = 0f;
            anomalyLogLimiter.Reset();
        }

        private bool UpdateTransportRates()
        {
            float now = Time.realtimeSinceStartup;
            if (!hasTransportRateSample ||
                totalDatagramsReceived < lastRateDatagramsReceived ||
                totalDatagramsSent < lastRateDatagramsSent)
            {
                hasTransportRateSample = true;
                lastTransportRateSampleTime = now;
                lastRateDatagramsReceived = totalDatagramsReceived;
                lastRateDatagramsSent = totalDatagramsSent;
                lastRateDatagramBytesReceived = totalDatagramBytesReceived;
                lastRateDatagramBytesSent = totalDatagramBytesSent;
                datagramsReceivedPerSecond = 0f;
                datagramsSentPerSecond = 0f;
                datagramBytesReceivedPerSecond = 0f;
                datagramBytesSentPerSecond = 0f;
                return false;
            }

            float elapsed = now - lastTransportRateSampleTime;
            if (elapsed < TransportRateSampleIntervalSeconds)
            {
                return false;
            }

            datagramsReceivedPerSecond = (totalDatagramsReceived - lastRateDatagramsReceived) / elapsed;
            datagramsSentPerSecond = (totalDatagramsSent - lastRateDatagramsSent) / elapsed;
            datagramBytesReceivedPerSecond = (totalDatagramBytesReceived - lastRateDatagramBytesReceived) / elapsed;
            datagramBytesSentPerSecond = (totalDatagramBytesSent - lastRateDatagramBytesSent) / elapsed;
            lastTransportRateSampleTime = now;
            lastRateDatagramsReceived = totalDatagramsReceived;
            lastRateDatagramsSent = totalDatagramsSent;
            lastRateDatagramBytesReceived = totalDatagramBytesReceived;
            lastRateDatagramBytesSent = totalDatagramBytesSent;
            return true;
        }

        private void EvaluateNetworkAnomalyWarnings()
        {
            if (!ShouldLog(IGPLogLevel.Warning) || kcp == null)
            {
                anomalyLogLimiter.Reset();
                return;
            }

            float now = Time.realtimeSinceStartup;
            float appLastRttMs = rttStats.SampleCount > 0 ? rttStats.LastRTT * 1000f : 0f;
            int kcpSmoothedRttMs = kcp.rx_srtt;
            bool highLatency = appLastRttMs >= IGPNetworkAnomalyThresholds.HighLatencyMs ||
                               kcpSmoothedRttMs >= IGPNetworkAnomalyThresholds.HighLatencyMs;
            bool latencyRecovered = appLastRttMs <= IGPNetworkAnomalyThresholds.HighLatencyRecoveryMs &&
                                    kcpSmoothedRttMs <= IGPNetworkAnomalyThresholds.HighLatencyRecoveryMs;
            anomalyLogLimiter.ObserveContinuous(
                "high-latency",
                highLatency,
                latencyRecovered,
                now,
                () => $"appLastRttMs={appLastRttMs:F1} appAvgRttMs={rttStats.AvgRTT * 1000f:F1} " +
                      $"kcpSrttMs={kcpSmoothedRttMs} kcpRtoMs={kcp.rx_rto} thresholdMs={IGPNetworkAnomalyThresholds.HighLatencyMs} " +
                      BuildKcpStateSummary(),
                message => LogWarning("anomaly", message),
                message => LogLifecycle("anomaly", message));

            bool highTraffic = datagramsReceivedPerSecond >= IGPNetworkAnomalyThresholds.HighDatagramsPerSecond ||
                               datagramsSentPerSecond >= IGPNetworkAnomalyThresholds.HighDatagramsPerSecond ||
                               datagramBytesReceivedPerSecond >= IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecond ||
                               datagramBytesSentPerSecond >= IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecond;
            bool trafficRecovered = datagramsReceivedPerSecond <= IGPNetworkAnomalyThresholds.HighDatagramsPerSecondRecovery &&
                                    datagramsSentPerSecond <= IGPNetworkAnomalyThresholds.HighDatagramsPerSecondRecovery &&
                                    datagramBytesReceivedPerSecond <= IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecondRecovery &&
                                    datagramBytesSentPerSecond <= IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecondRecovery;
            anomalyLogLimiter.ObserveContinuous(
                "high-traffic",
                highTraffic,
                trafficRecovered,
                now,
                () => $"datagramsInPerSecond={datagramsReceivedPerSecond:F0} datagramsOutPerSecond={datagramsSentPerSecond:F0} " +
                      $"bytesInPerSecond={datagramBytesReceivedPerSecond:F0} bytesOutPerSecond={datagramBytesSentPerSecond:F0} " +
                      $"datagramThresholdPerSecond={IGPNetworkAnomalyThresholds.HighDatagramsPerSecond:F0} " +
                      $"byteThresholdPerSecond={IGPNetworkAnomalyThresholds.HighDatagramBytesPerSecond:F0} " +
                      BuildKcpStateSummary(),
                message => LogWarning("anomaly", message),
                message => LogLifecycle("anomaly", message));
        }

        private string BuildKcpStateSummary()
        {
            if (kcp == null)
            {
                return "kcp(null)";
            }

            return
                $"kcp(conv={conversationId},waitSnd={kcp.WaitSnd},sndQueue={kcp.snd_queue.Count},sndBuf={kcp.snd_buf.Count}," +
                $"rcvQueue={kcp.rcv_queue.Count},rcvBuf={kcp.rcv_buf.Count},ackList={kcp.acklist.Count}," +
                $"sndUna={kcp.snd_una},sndNxt={kcp.snd_nxt},rcvNxt={kcp.rcv_nxt}," +
                $"sndWnd={kcp.snd_wnd},rcvWnd={kcp.rcv_wnd},remoteWnd={kcp.rmt_wnd},cwnd={kcp.cwnd}," +
                $"mtu={kcp.mtu},mss={kcp.mss},rto={kcp.rx_rto},srtt={kcp.rx_srtt},rttVar={kcp.rx_rttval}," +
                $"intervalMs={kcp.interval},nextUpdateInMs={GetNextUpdateDelayMs()},state={kcp.state})";
        }

        private int GetNextUpdateDelayMs()
        {
            if (kcp == null)
            {
                return 0;
            }

            return Math.Max(0, unchecked((int)(nextUpdate - NowMs())));
        }

        private static int NormalizeWindowSize(int value)
        {
            return Math.Min(MaxWindowSize, Math.Max(MinWindowSize, value));
        }

    }
}
