#nullable enable
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IGP.Multiplayer.Core;
using IGP.Multiplayer.Models;
using IGP.Multiplayer.Protocol;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
using IGPLog = IGP.Multiplayer.IGPMultiplayerLog;

namespace IGP.Multiplayer
{
    internal sealed class IGPTcpClient : IDisposable
    {
        internal const int DefaultFrameMaxBytes = 64 * 1024;
        internal const int HardFrameMaxBytes = 1024 * 1024;
        private const int ConnectTimeoutMilliseconds = 5000;
        private const int ReadChunkBytes = 16 * 1024;
        private const int MaxFramesPerTick = 256;
        private const int MaxBytesPerTick = 1024 * 1024;
        private const double MaxReceiveMillisecondsPerTick = 2.0;
        private const int OutboundFrameLimit = 2048;
        private const int OutboundByteLimit = 32 * 1024 * 1024;
        private const double IdleProbeIntervalSeconds = 1.0;
        private const double IdleProbeExpirationSeconds = 4.0;
        private const int MaxPendingIdleProbes = 4;
        private const double DelayedProbeLogIntervalSeconds = 30.0;

        private readonly object connectLock = new object();
        private readonly Queue<QueuedFrame> outbound = new Queue<QueuedFrame>();
        private readonly Dictionary<int, PendingProbe> pendingProbes = new Dictionary<int, PendingProbe>();
        private readonly byte[] readChunk = new byte[ReadChunkBytes];
        private Socket? socket;
        private Socket? connectingSocket;
        private CancellationTokenSource? connectCancellation;
        private Task<ConnectOutcome>? connectTask;
        private byte[] receiveBuffer = Array.Empty<byte>();
        private int receiveCount;
        private int outboundBytes;
        private int connectGeneration;
        private bool connected;
        private bool faultDispatched;
        private bool disposed;
        private int frameMaxBytes = DefaultFrameMaxBytes;
        private IGPDataPlanePeerTable? peers;
        private IGPRTTStats appRttStats = new IGPRTTStats();
        private int idleProbeSequence;
        private double lastActivityAt;
        private double lastIdleProbeAttemptAt;
        private long framesReceived;
        private long framesSent;
        private long bytesReceived;
        private long bytesSent;
        private long partialSendCount;
        private long handshakeDurationMilliseconds;
        private long expiredProbeCount;
        private double nextDelayedProbeLogAt;
        private string lastSocketError = string.Empty;

        private sealed class QueuedFrame
        {
            public QueuedFrame(byte[] bytes)
            {
                Bytes = bytes;
            }

            public byte[] Bytes { get; }
            public int Offset { get; set; }
        }

        private readonly struct PendingProbe
        {
            public PendingProbe(double sentAt)
            {
                SentAt = sentAt;
            }

            public double SentAt { get; }
        }

        private sealed class ConnectOutcome
        {
            public int Generation { get; set; }
            public Socket? Socket { get; set; }
            public IGPDataPlanePeerTable? Peers { get; set; }
            public string ErrorCode { get; set; } = string.Empty;
            public string ErrorMessage { get; set; } = string.Empty;
            public object? Details { get; set; }
            public long HandshakeDurationMilliseconds { get; set; }
            public bool Success => Socket != null;
        }

        private sealed class TcpConnectException : Exception
        {
            public TcpConnectException(string code, string message, Exception? inner = null)
                : base(message, inner)
            {
                Code = code;
            }

            public string Code { get; }
        }

        public string RoomId { get; private set; } = string.Empty;
        public string PlayerId { get; private set; } = string.Empty;
        public bool IsConnected => connected && socket != null && !faultDispatched;
        public bool IsAlive => IsConnected;
        public bool IsWritable => IsConnected && outbound.Count < OutboundFrameLimit && outboundBytes < OutboundByteLimit;
        public int FrameMaxBytes => frameMaxBytes;
        public int NegotiatedEnvelopeVersion => IsConnected ? IGPDataPlaneEnvelopeCodec.Version : 0;
        public IGPRTTStats AppRTTStats => appRttStats;
        public string DiagnosticsSummary =>
            $"transport=tcp connected={IsConnected} framesIn={framesReceived} framesOut={framesSent} " +
            $"bytesIn={bytesReceived} bytesOut={bytesSent} pendingFrames={outbound.Count} pendingBytes={outboundBytes} " +
            $"partialSends={partialSendCount} handshakeMs={handshakeDurationMilliseconds} " +
            $"expiredProbes={expiredProbeCount} frameMaxBytes={frameMaxBytes} lastSocketError={lastSocketError}";

        internal event Action<IGPTransportMessageReceived>? MessageReceived;
        internal event Action<IGPTransportConnectionChanged>? ConnectionChanged;
        internal event Action<IGPTransportFault>? Faulted;

        public void Connect(string host, int port, string roomId, string playerId, string token, int requestedFrameMaxBytes)
        {
            if (disposed) throw new ObjectDisposedException(nameof(IGPTcpClient));
            if (string.IsNullOrWhiteSpace(host) || port < 1 || port > ushort.MaxValue ||
                string.IsNullOrWhiteSpace(roomId) || string.IsNullOrWhiteSpace(playerId) || string.IsNullOrWhiteSpace(token))
            {
                throw new ArgumentException("Invalid hosted TCP descriptor.");
            }
            if (requestedFrameMaxBytes <= IGPDataPlaneMessageCodec.MaxEnvelopeHeaderBytes ||
                requestedFrameMaxBytes > HardFrameMaxBytes)
            {
                throw new ArgumentOutOfRangeException(nameof(requestedFrameMaxBytes));
            }

            DisconnectInternal(notifyConnectionStateChanged: false);
            RoomId = roomId;
            PlayerId = playerId;
            frameMaxBytes = requestedFrameMaxBytes;
            receiveBuffer = new byte[frameMaxBytes + 4];
            appRttStats = new IGPRTTStats();
            faultDispatched = false;
            framesReceived = 0;
            framesSent = 0;
            bytesReceived = 0;
            bytesSent = 0;
            partialSendCount = 0;
            handshakeDurationMilliseconds = 0;
            expiredProbeCount = 0;
            nextDelayedProbeLogAt = 0;
            lastSocketError = string.Empty;
            int generation = ++connectGeneration;
            connectCancellation = new CancellationTokenSource();
            CancellationToken cancellationToken = connectCancellation.Token;
            connectTask = Task.Run(
                () => ConnectWorker(generation, host, port, playerId, token, frameMaxBytes, cancellationToken),
                cancellationToken);
        }

        public void Tick()
        {
            if (disposed) return;
            CompleteConnectIfReady();
            if (!IsConnected) return;

            FlushOutbound();
            if (!IsConnected) return;
            ReceiveFrames();
            if (!IsConnected) return;
            ExpireIdleProbes();
            DriveIdleProbe();
        }

        public IGPTransportSendResult TrySendMessages(IReadOnlyList<Message> messages, bool isControl)
        {
            if (messages == null || messages.Count == 0) return IGPTransportSendResult.InvalidPayload;
            if (faultDispatched) return IGPTransportSendResult.Faulted;
            if (!IsConnected || peers == null) return IGPTransportSendResult.Unavailable;

            var frames = new List<byte[]>(messages.Count);
            int totalBytes = 0;
            try
            {
                foreach (Message message in messages)
                {
                    if (message == null) return IGPTransportSendResult.InvalidPayload;
                    byte[] payload = IGPDataPlaneMessageCodec.Encode(message, peers);
                    if (payload.Length == 0 || payload.Length > frameMaxBytes)
                    {
                        return IGPTransportSendResult.InvalidPayload;
                    }
                    byte[] frame = EncodeFrame(payload);
                    totalBytes = checked(totalBytes + frame.Length);
                    frames.Add(frame);
                    if (frames.Count > OutboundFrameLimit ||
                        totalBytes > OutboundByteLimit ||
                        outbound.Count + frames.Count > OutboundFrameLimit ||
                        outboundBytes + totalBytes > OutboundByteLimit)
                    {
                        return IGPTransportSendResult.WouldBlock;
                    }
                }
            }
            catch
            {
                return IGPTransportSendResult.InvalidPayload;
            }

            foreach (byte[] frame in frames) outbound.Enqueue(new QueuedFrame(frame));
            outboundBytes += totalBytes;
            return IGPTransportSendResult.Accepted;
        }

        internal void ApplyGameActivation(
            byte playerSlot,
            byte hostSlot,
            IReadOnlyDictionary<string, byte> playerSlots)
        {
            var reverse = new Dictionary<byte, string>();
            foreach (KeyValuePair<string, byte> entry in playerSlots)
            {
                if (string.IsNullOrWhiteSpace(entry.Key) || entry.Value == 0 || reverse.ContainsKey(entry.Value))
                {
                    throw new InvalidOperationException("Game activation slot table is invalid.");
                }
                reverse.Add(entry.Value, entry.Key);
            }
            if (hostSlot == 0 || playerSlot == 0 ||
                !reverse.TryGetValue(playerSlot, out string? playerId) ||
                !string.Equals(playerId, PlayerId, StringComparison.Ordinal) ||
                !reverse.ContainsKey(hostSlot))
            {
                throw new InvalidOperationException("Game activation does not contain the authenticated player.");
            }
            peers = new IGPDataPlanePeerTable(playerSlot, hostSlot, playerSlots, reverse);
        }

        internal void ApplyGameDeactivation()
        {
            peers = null;
        }

        public void Disconnect()
        {
            DisconnectInternal(notifyConnectionStateChanged: true);
        }

        public void Dispose()
        {
            if (disposed) return;
            disposed = true;
            DisconnectInternal(notifyConnectionStateChanged: false);
        }

        internal static byte[] EncodeFrame(byte[] payload)
        {
            if (payload == null || payload.Length == 0) throw new ArgumentOutOfRangeException(nameof(payload));
            var frame = new byte[payload.Length + 4];
            BinaryPrimitives.WriteUInt32BigEndian(frame.AsSpan(0, 4), checked((uint)payload.Length));
            Buffer.BlockCopy(payload, 0, frame, 4, payload.Length);
            return frame;
        }

        private ConnectOutcome ConnectWorker(
            int generation,
            string host,
            int port,
            string expectedPlayerId,
            string token,
            int maxFrameBytes,
            CancellationToken cancellationToken)
        {
            var handshakeStopwatch = Stopwatch.StartNew();
            try
            {
                DateTime deadline = DateTime.UtcNow.AddMilliseconds(ConnectTimeoutMilliseconds);
                Task<IPAddress[]> resolveTask = Dns.GetHostAddressesAsync(host);
                WaitHandle resolveWaitHandle = ((IAsyncResult)resolveTask).AsyncWaitHandle;
                int resolveResult = WaitHandle.WaitAny(
                    new[] { resolveWaitHandle, cancellationToken.WaitHandle },
                    RemainingMilliseconds(deadline));
                if (resolveResult == WaitHandle.WaitTimeout)
                {
                    throw new TcpConnectException("TCP_HANDSHAKE_TIMEOUT", "TCP connection or handshake timed out.");
                }
                if (resolveResult == 1) throw new OperationCanceledException(cancellationToken);
                IPAddress[] addresses = resolveTask.GetAwaiter().GetResult();
                Array.Sort(addresses, (left, right) => AddressRank(left).CompareTo(AddressRank(right)));
                if (addresses.Length == 0)
                {
                    throw new TcpConnectException("TCP_CONNECT_FAILED", "TCP host could not be resolved.");
                }

                Exception? lastConnectError = null;
                foreach (IPAddress address in addresses)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    var candidate = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
                    {
                        NoDelay = true,
                        SendTimeout = RemainingMilliseconds(deadline),
                        ReceiveTimeout = RemainingMilliseconds(deadline),
                    };
                    candidate.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
                    lock (connectLock) connectingSocket = candidate;
                    try
                    {
                        ConnectWithTimeout(candidate, address, port, deadline, cancellationToken);
                        SendBlockingFrame(candidate, BuildHandshake(token), maxFrameBytes, deadline, cancellationToken);
                        byte[] ackBytes = ReceiveBlockingFrame(candidate, maxFrameBytes, deadline, cancellationToken);
                        IGPDataPlanePeerTable? peerTable = ParseHandshakeAck(ackBytes, expectedPlayerId);
                        candidate.Blocking = false;
                        lock (connectLock)
                        {
                            if (ReferenceEquals(connectingSocket, candidate)) connectingSocket = null;
                        }
                        return new ConnectOutcome
                        {
                            Generation = generation,
                            Socket = candidate,
                            Peers = peerTable,
                            HandshakeDurationMilliseconds = handshakeStopwatch.ElapsedMilliseconds,
                        };
                    }
                    catch (Exception ex)
                    {
                        lastConnectError = ex;
                        try { candidate.Close(0); } catch { }
                        lock (connectLock)
                        {
                            if (ReferenceEquals(connectingSocket, candidate)) connectingSocket = null;
                        }
                        if (ex is TcpConnectException || ex is OperationCanceledException || DateTime.UtcNow >= deadline) throw;
                    }
                }

                throw new TcpConnectException("TCP_CONNECT_FAILED", "TCP connection failed.", lastConnectError);
            }
            catch (OperationCanceledException ex)
            {
                return new ConnectOutcome { Generation = generation, ErrorCode = "TCP_CONNECT_FAILED", ErrorMessage = "TCP connection was canceled.", Details = ex, HandshakeDurationMilliseconds = handshakeStopwatch.ElapsedMilliseconds };
            }
            catch (TcpConnectException ex)
            {
                return new ConnectOutcome { Generation = generation, ErrorCode = ex.Code, ErrorMessage = ex.Message, Details = ex, HandshakeDurationMilliseconds = handshakeStopwatch.ElapsedMilliseconds };
            }
            catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
            {
                return new ConnectOutcome { Generation = generation, ErrorCode = "TCP_HANDSHAKE_TIMEOUT", ErrorMessage = "TCP connection or handshake timed out.", Details = ex, HandshakeDurationMilliseconds = handshakeStopwatch.ElapsedMilliseconds };
            }
            catch (Exception ex)
            {
                return new ConnectOutcome { Generation = generation, ErrorCode = "TCP_CONNECT_FAILED", ErrorMessage = $"TCP connection failed: {ex.Message}", Details = ex, HandshakeDurationMilliseconds = handshakeStopwatch.ElapsedMilliseconds };
            }
        }

        private void CompleteConnectIfReady()
        {
            Task<ConnectOutcome>? pending = connectTask;
            if (pending == null || !pending.IsCompleted) return;
            connectTask = null;
            ConnectOutcome outcome;
            try
            {
                outcome = pending.GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                outcome = new ConnectOutcome
                {
                    Generation = connectGeneration,
                    ErrorCode = "TCP_CONNECT_FAILED",
                    ErrorMessage = $"TCP connection failed: {ex.Message}",
                    Details = ex,
                };
            }

            connectCancellation?.Dispose();
            connectCancellation = null;
            handshakeDurationMilliseconds = outcome.HandshakeDurationMilliseconds;
            if (outcome.Generation != connectGeneration ||
                disposed ||
                string.IsNullOrWhiteSpace(RoomId) ||
                string.IsNullOrWhiteSpace(PlayerId))
            {
                try { outcome.Socket?.Close(0); } catch { }
                return;
            }
            if (!outcome.Success)
            {
                DispatchFault(outcome.ErrorCode, outcome.ErrorMessage, outcome.Details);
                return;
            }

            socket = outcome.Socket;
            peers = outcome.Peers;
            connected = true;
            faultDispatched = false;
            lastActivityAt = Time.realtimeSinceStartupAsDouble;
            lastIdleProbeAttemptAt = lastActivityAt;
            ConnectionChanged?.Invoke(new IGPTransportConnectionChanged(IGPRealtimeTransport.Tcp, true, "handshake-ack"));
        }

        private void FlushOutbound()
        {
            Socket? activeSocket = socket;
            if (activeSocket == null) return;
            while (outbound.Count > 0)
            {
                QueuedFrame current = outbound.Peek();
                try
                {
                    int sent = activeSocket.Send(
                        current.Bytes,
                        current.Offset,
                        current.Bytes.Length - current.Offset,
                        SocketFlags.None);
                    if (sent <= 0)
                    {
                        DispatchFault("TCP_SEND_FAILED", "TCP socket closed while sending.");
                        return;
                    }
                    current.Offset += sent;
                    if (current.Offset < current.Bytes.Length) partialSendCount += 1;
                    outboundBytes -= sent;
                    bytesSent += sent;
                    lastActivityAt = Time.realtimeSinceStartupAsDouble;
                    if (current.Offset == current.Bytes.Length)
                    {
                        outbound.Dequeue();
                        framesSent += 1;
                    }
                }
                catch (SocketException ex) when (IsWouldBlock(ex.SocketErrorCode))
                {
                    return;
                }
                catch (Exception ex)
                {
                    DispatchFault("TCP_SEND_FAILED", $"TCP send failed: {ex.Message}", ex);
                    return;
                }
            }
        }

        private void ReceiveFrames()
        {
            var stopwatch = Stopwatch.StartNew();
            int processedFrames = 0;
            int processedBytes = 0;
            if (!ProcessBufferedFrames(stopwatch, ref processedFrames, ref processedBytes)) return;

            Socket? activeSocket = socket;
            if (activeSocket == null) return;
            while (processedFrames < MaxFramesPerTick &&
                   processedBytes < MaxBytesPerTick &&
                   stopwatch.Elapsed.TotalMilliseconds < MaxReceiveMillisecondsPerTick)
            {
                int capacity = receiveBuffer.Length - receiveCount;
                if (capacity <= 0)
                {
                    DispatchFault("TCP_FRAME_INVALID", "TCP receive buffer exceeded the negotiated frame limit.");
                    return;
                }

                int requested = Math.Min(readChunk.Length, capacity);
                try
                {
                    int received = activeSocket.Receive(readChunk, 0, requested, SocketFlags.None);
                    if (received == 0)
                    {
                        if (receiveCount > 0)
                        {
                            DispatchFault("TCP_FRAME_INVALID", "TCP connection closed before the current frame was complete.");
                        }
                        else
                        {
                            DispatchFault("TCP_RECEIVE_FAILED", "TCP connection was closed by the remote endpoint.");
                        }
                        return;
                    }
                    Buffer.BlockCopy(readChunk, 0, receiveBuffer, receiveCount, received);
                    receiveCount += received;
                    bytesReceived += received;
                    lastActivityAt = Time.realtimeSinceStartupAsDouble;
                    if (!ProcessBufferedFrames(stopwatch, ref processedFrames, ref processedBytes)) return;
                }
                catch (SocketException ex) when (IsWouldBlock(ex.SocketErrorCode))
                {
                    return;
                }
                catch (Exception ex)
                {
                    DispatchFault("TCP_RECEIVE_FAILED", $"TCP receive failed: {ex.Message}", ex);
                    return;
                }
            }
        }

        private bool ProcessBufferedFrames(Stopwatch stopwatch, ref int processedFrames, ref int processedBytes)
        {
            while (receiveCount >= 4 &&
                   processedFrames < MaxFramesPerTick &&
                   processedBytes < MaxBytesPerTick &&
                   stopwatch.Elapsed.TotalMilliseconds < MaxReceiveMillisecondsPerTick)
            {
                uint declaredLength = BinaryPrimitives.ReadUInt32BigEndian(receiveBuffer.AsSpan(0, 4));
                if (declaredLength == 0 || declaredLength > frameMaxBytes)
                {
                    DispatchFault("TCP_FRAME_INVALID", "TCP frame exceeds the negotiated limit.", declaredLength);
                    return false;
                }
                int frameLength = checked((int)declaredLength);
                if (receiveCount < frameLength + 4) return true;

                var payload = new byte[frameLength];
                Buffer.BlockCopy(receiveBuffer, 4, payload, 0, frameLength);
                int remaining = receiveCount - frameLength - 4;
                if (remaining > 0) Buffer.BlockCopy(receiveBuffer, frameLength + 4, receiveBuffer, 0, remaining);
                receiveCount = remaining;
                processedFrames += 1;
                processedBytes += frameLength;
                framesReceived += 1;

                Message message;
                try
                {
                    if (peers == null) throw new InvalidOperationException("TCP peer table is unavailable.");
                    message = IGPDataPlaneMessageCodec.Decode(payload, RoomId, IGPRealtimeTransport.Tcp, peers);
                }
                catch (Exception ex)
                {
                    DispatchFault("TCP_FRAME_INVALID", $"TCP data-plane frame is invalid: {ex.Message}", ex);
                    return false;
                }
                if (string.Equals(message.type, "pong", StringComparison.OrdinalIgnoreCase))
                {
                    HandlePong(message);
                    continue;
                }
                MessageReceived?.Invoke(new IGPTransportMessageReceived(IGPRealtimeTransport.Tcp, message));
            }
            return true;
        }

        private void DriveIdleProbe()
        {
            double now = Time.realtimeSinceStartupAsDouble;
            if (!IsConnected || outbound.Count > 0 ||
                now - lastActivityAt < IdleProbeIntervalSeconds ||
                now - lastIdleProbeAttemptAt < IdleProbeIntervalSeconds)
            {
                return;
            }

            while (pendingProbes.Count >= MaxPendingIdleProbes) EvictOldestProbe();
            int sequence = idleProbeSequence++;
            var probe = new Message
            {
                type = "ping",
                roomId = RoomId,
                playerId = PlayerId,
                reliable = true,
                content = new IGPReliableHeartbeatPayload(
                    IGPReliableHeartbeatPayloadCodec.Version1,
                    unchecked((uint)sequence)),
            };
            lastIdleProbeAttemptAt = now;
            if (TrySendMessages(new[] { probe }, isControl: true) == IGPTransportSendResult.Accepted)
            {
                pendingProbes[sequence] = new PendingProbe(now);
            }
        }

        private void HandlePong(Message message)
        {
            int? sequence = message.content is IGPReliableHeartbeatPayload binaryContent
                ? unchecked((int)binaryContent.Sequence)
                : (message.content as JObject)?.Value<int?>("seq");
            if (!sequence.HasValue || !pendingProbes.TryGetValue(sequence.Value, out PendingProbe probe)) return;
            pendingProbes.Remove(sequence.Value);
            double now = Time.realtimeSinceStartupAsDouble;
            appRttStats.AddSample((float)Math.Max(0, now - probe.SentAt));
        }

        private void ExpireIdleProbes()
        {
            double now = Time.realtimeSinceStartupAsDouble;
            bool expired = false;
            while (GetOldestProbeAge(now) >= IdleProbeExpirationSeconds)
            {
                expired |= EvictOldestProbe();
                expiredProbeCount += 1;
            }
            if (expired && now >= nextDelayedProbeLogAt)
            {
                nextDelayedProbeLogAt = now + DelayedProbeLogIntervalSeconds;
                IGPLog.Warning(
                    "tcp",
                    "idle-probe",
                    $"event=network-anomaly kind=arena-pong-response-delayed pending={pendingProbes.Count} " +
                    $"expiredTotal={expiredProbeCount} transport=tcp");
            }
        }

        private double GetOldestProbeAge(double now)
        {
            double oldest = double.MaxValue;
            foreach (PendingProbe probe in pendingProbes.Values) oldest = Math.Min(oldest, probe.SentAt);
            return oldest == double.MaxValue ? 0 : Math.Max(0, now - oldest);
        }

        private bool EvictOldestProbe()
        {
            int oldestSequence = 0;
            double oldest = double.MaxValue;
            bool found = false;
            foreach (KeyValuePair<int, PendingProbe> entry in pendingProbes)
            {
                if (!found || entry.Value.SentAt < oldest)
                {
                    found = true;
                    oldest = entry.Value.SentAt;
                    oldestSequence = entry.Key;
                }
            }
            return found && pendingProbes.Remove(oldestSequence);
        }

        private void DispatchFault(string code, string message, object? details = null)
        {
            if (faultDispatched) return;
            faultDispatched = true;
            connected = false;
            if (details is SocketException socketException)
            {
                lastSocketError = socketException.SocketErrorCode.ToString();
            }
            else if (details is Exception exception)
            {
                lastSocketError = exception.GetType().Name;
            }
            CloseActiveSocket();
            outbound.Clear();
            outboundBytes = 0;
            pendingProbes.Clear();
            Faulted?.Invoke(new IGPTransportFault(IGPRealtimeTransport.Tcp, code, message, true, details));
        }

        private void DisconnectInternal(bool notifyConnectionStateChanged)
        {
            bool hadAttempt = connected || connectTask != null || socket != null || connectingSocket != null;
            Task<ConnectOutcome>? abandonedConnectTask = connectTask;
            CancellationTokenSource? abandonedCancellation = connectCancellation;
            connected = false;
            faultDispatched = false;
            connectGeneration += 1;
            try { abandonedCancellation?.Cancel(); } catch { }
            lock (connectLock)
            {
                try { connectingSocket?.Close(0); } catch { }
                connectingSocket = null;
            }
            CloseActiveSocket();
            connectTask = null;
            connectCancellation = null;
            if (abandonedConnectTask != null)
            {
                _ = abandonedConnectTask.ContinueWith(
                    completed => CleanupAbandonedConnect(completed, abandonedCancellation),
                    CancellationToken.None,
                    TaskContinuationOptions.ExecuteSynchronously,
                    TaskScheduler.Default);
            }
            else
            {
                abandonedCancellation?.Dispose();
            }
            receiveCount = 0;
            outbound.Clear();
            outboundBytes = 0;
            pendingProbes.Clear();
            peers = null;
            RoomId = string.Empty;
            PlayerId = string.Empty;
            if (notifyConnectionStateChanged && hadAttempt)
            {
                ConnectionChanged?.Invoke(new IGPTransportConnectionChanged(IGPRealtimeTransport.Tcp, false, "disconnected"));
            }
        }

        private void CloseActiveSocket()
        {
            try { socket?.Shutdown(SocketShutdown.Both); } catch { }
            try { socket?.Close(0); } catch { }
            socket = null;
        }

        private static byte[] BuildHandshake(string token)
        {
            var handshake = new JObject
            {
                ["type"] = "reliable_handshake",
                ["version"] = 1,
                ["token"] = token,
                ["supportedEnvelopeVersions"] = new JArray(IGPDataPlaneEnvelopeCodec.Version),
            };
            return Encoding.UTF8.GetBytes(handshake.ToString(Formatting.None));
        }

        private static IGPDataPlanePeerTable? ParseHandshakeAck(byte[] ackBytes, string expectedPlayerId)
        {
            JObject ack;
            try
            {
                ack = JObject.Parse(Encoding.UTF8.GetString(ackBytes));
            }
            catch (Exception ex)
            {
                throw new TcpConnectException("TCP_AUTH_REJECTED", "TCP transport returned an invalid acknowledgement.", ex);
            }
            if (!string.Equals(ack.Value<string>("type"), "reliable_handshake_ack", StringComparison.Ordinal))
            {
                throw new TcpConnectException("TCP_AUTH_REJECTED", "TCP transport authentication was rejected.");
            }
            if (ack.Value<int?>("selectedEnvelopeVersion") != IGPDataPlaneEnvelopeCodec.Version)
            {
                throw new TcpConnectException("TCP_ENVELOPE_NEGOTIATION_FAILED", "TCP transport did not select DataPlaneEnvelope V1.");
            }
            if (ack.Value<int?>("playerSlot").GetValueOrDefault() == 0)
            {
                return null;
            }
            try
            {
                return IGPDataPlanePeerTable.FromHandshakeAck(ack, expectedPlayerId);
            }
            catch (Exception ex)
            {
                throw new TcpConnectException("TCP_AUTH_REJECTED", ex.Message, ex);
            }
        }

        private static void ConnectWithTimeout(
            Socket candidate,
            IPAddress address,
            int port,
            DateTime deadline,
            CancellationToken cancellationToken)
        {
            IAsyncResult asyncResult = candidate.BeginConnect(new IPEndPoint(address, port), null, null);
            int waitResult = WaitHandle.WaitAny(
                new[] { asyncResult.AsyncWaitHandle, cancellationToken.WaitHandle },
                RemainingMilliseconds(deadline));
            if (waitResult == WaitHandle.WaitTimeout)
            {
                throw new TcpConnectException("TCP_HANDSHAKE_TIMEOUT", "TCP connection or handshake timed out.");
            }
            if (waitResult == 1) throw new OperationCanceledException(cancellationToken);
            candidate.EndConnect(asyncResult);
        }

        private static void SendBlockingFrame(
            Socket candidate,
            byte[] payload,
            int maxFrameBytes,
            DateTime deadline,
            CancellationToken cancellationToken)
        {
            if (payload.Length == 0 || payload.Length > maxFrameBytes)
            {
                throw new TcpConnectException("TCP_FRAME_INVALID", "TCP handshake exceeds the negotiated frame limit.");
            }
            byte[] frame = EncodeFrame(payload);
            int offset = 0;
            while (offset < frame.Length)
            {
                cancellationToken.ThrowIfCancellationRequested();
                candidate.SendTimeout = RemainingMilliseconds(deadline);
                int sent = candidate.Send(frame, offset, frame.Length - offset, SocketFlags.None);
                if (sent <= 0) throw new TcpConnectException("TCP_SEND_FAILED", "TCP socket closed while sending the handshake.");
                offset += sent;
            }
        }

        private static byte[] ReceiveBlockingFrame(
            Socket candidate,
            int maxFrameBytes,
            DateTime deadline,
            CancellationToken cancellationToken)
        {
            var header = new byte[4];
            ReceiveExact(candidate, header, deadline, cancellationToken);
            uint length = BinaryPrimitives.ReadUInt32BigEndian(header);
            if (length == 0 || length > maxFrameBytes)
            {
                throw new TcpConnectException("TCP_FRAME_INVALID", "TCP handshake acknowledgement exceeds the negotiated frame limit.");
            }
            var payload = new byte[checked((int)length)];
            ReceiveExact(candidate, payload, deadline, cancellationToken);
            return payload;
        }

        private static void ReceiveExact(
            Socket candidate,
            byte[] buffer,
            DateTime deadline,
            CancellationToken cancellationToken)
        {
            int offset = 0;
            while (offset < buffer.Length)
            {
                cancellationToken.ThrowIfCancellationRequested();
                candidate.ReceiveTimeout = RemainingMilliseconds(deadline);
                int received = candidate.Receive(buffer, offset, buffer.Length - offset, SocketFlags.None);
                if (received <= 0)
                {
                    throw new TcpConnectException("TCP_AUTH_REJECTED", "TCP transport closed during the handshake.");
                }
                offset += received;
            }
        }

        private static int RemainingMilliseconds(DateTime deadline)
        {
            int remaining = (int)Math.Ceiling((deadline - DateTime.UtcNow).TotalMilliseconds);
            if (remaining <= 0)
            {
                throw new TcpConnectException("TCP_HANDSHAKE_TIMEOUT", "TCP connection or handshake timed out.");
            }
            return remaining;
        }

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

        private static int AddressRank(IPAddress address) =>
            address.AddressFamily == AddressFamily.InterNetworkV6 ? 0 :
            address.AddressFamily == AddressFamily.InterNetwork ? 1 : 2;

        private static void CleanupAbandonedConnect(
            Task<ConnectOutcome> completed,
            CancellationTokenSource? cancellation)
        {
            try
            {
                if (completed.Status == TaskStatus.RanToCompletion)
                {
                    try { completed.Result.Socket?.Close(0); } catch { }
                }
                else if (completed.IsFaulted)
                {
                    _ = completed.Exception;
                }
            }
            finally
            {
                cancellation?.Dispose();
            }
        }
    }
}
