#nullable enable
using IGP.UnitySDK.Models;
using IGP.UnitySDK;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
using IGP.UnitySDK.Abstractions;
using IGP.Multiplayer.Models;
using IGP.Multiplayer.Core;
using IGP.Multiplayer.Network;
using IGP.Multiplayer.Protocol;

namespace IGP.Multiplayer
{
    public partial class IGPMultiplayerRuntime
    {

        private void RecoverHostedDataPlaneStatusIfAlive()
        {
            if (!string.Equals(currentHostedDataPlaneStatus, "error", StringComparison.OrdinalIgnoreCase) ||
                !IsKcpAlive)
            {
                return;
            }

            currentHostedDataPlaneStatus = IsKcpConnected ? "connected" : "connecting";
            currentHostedDataPlaneError = string.Empty;
            UpdateRealtimeConnectionState(IsKcpConnected
                ? IGPRealtimeConnectionState.Ready
                : IGPRealtimeConnectionState.Connecting);
        }

        private void ApplyKcpRuntimeSettings()
        {
            if (KcpClient != null)
            {
                KcpClient.MaxDatagramsPerTick = KcpMaxDatagramsPerTick;
                KcpClient.SendWindowSize = KcpSendWindowSize;
                KcpClient.ReceiveWindowSize = KcpReceiveWindowSize;
            }
        }

        internal async Task ConnectDataPlaneAsync()
        {
            EnsureSDKInitialized();

            LogSdkInfo(
                "realtime",
                $"event=connect mode={currentDataPlaneMode} " +
                $"transportAttached={dataPlaneTransport != null} hostedSession={IsHostedSessionAttached} " +
                $"realtimeState={realtimeConnectionState} roomId={currentRoomId}");

            if (IsKcpConnected && currentDataPlaneMode != IGPDataPlaneMode.None && dataPlaneTransport != null)
            {
                LogSdkInfo("realtime", "event=connect-skipped reason=already-ready");
                return;
            }

            if (!IsHostedSessionAttached)
            {
                throw new IGPSDKException(
                    "Room context is required before the realtime data plane can be connected",
                    IGPMultiplayerErrorCodes.ERR_ROOM_CONTEXT_REQUIRED);
            }

            if (string.IsNullOrWhiteSpace(currentRoomId))
            {
                throw new IGPSDKException("Room id is required before hosted data plane can be requested");
            }

            if (KcpClient == null)
            {
                currentHostedDataPlaneStatus = "skipped:no-kcp-client";
                currentHostedDataPlaneError = "KCP client is not initialized";
                UpdateRealtimeConnectionState(IGPRealtimeConnectionState.Failed);
                throw new IGPSDKException(currentHostedDataPlaneError);
            }

            if (string.IsNullOrWhiteSpace(playerId))
            {
                throw new IGPSDKException("Player id is required before hosted data plane can be requested");
            }

            if (dataPlaneTransport != null || currentDataPlaneMode != IGPDataPlaneMode.None)
            {
                ResetHostedDataPlaneState("reconnecting");
            }

            await RequestAndAttachHostedDataPlaneAsync(currentRoomId, playerId, realtimeRoomGeneration);
        }


        /// <summary>
        /// Internal helper to send P2P messages using the best available transport.
        /// </summary>
        internal bool TrySendP2PMessage(Message msg)
        {
            return TrySendP2PMessages(new[] { msg }, isControl: false) == IGPTransportSendResult.Accepted;
        }

        internal IGPTransportSendResult TrySendP2PMessages(IReadOnlyList<Message> messages, bool isControl)
        {
            var transport = dataPlaneTransport;
            if (IsKcpConnected && transport != null)
            {
                return transport.TrySendMessages(messages, isControl);
            }

            if (realtimeConnectionState == IGPRealtimeConnectionState.RequestingDescriptor ||
                realtimeConnectionState == IGPRealtimeConnectionState.Connecting)
            {
                return IGPTransportSendResult.WouldBlock;
            }

            if (realtimeConnectionState == IGPRealtimeConnectionState.Ready)
            {
                _ = MarkHostedDataPlaneInterrupted(
                    string.IsNullOrWhiteSpace(currentHostedDataPlaneError)
                        ? "Connection interrupted: direct data connection is unavailable"
                        : currentHostedDataPlaneError);
            }

            return IGPTransportSendResult.Unavailable;
        }

        internal bool TrySendUnreliableP2PMessage(Message msg)
        {
            var client = UnreliableUdpClient;
            if (client == null || !client.IsConnected)
            {
                return false;
            }

            return client.TrySendMessage(msg);
        }

        internal bool IsP2PDataPlaneReady => IsKcpConnected && dataPlaneTransport != null;
        internal bool IsP2PDataPlaneWritable => IsP2PDataPlaneReady && dataPlaneTransport?.IsWritable == true;

        internal void SendP2PMessage(Message msg)
        {
            if (!TrySendP2PMessage(msg))
            {
                throw new IGPSDKException("Direct data connection failed to queue the message");
            }
        }


        private IGPSDKException MarkHostedDataPlaneInterrupted(
            string errorMessage,
            string errorCode = "HOSTED_DATA_PLANE_INTERRUPTED",
            IGPRealtimeTransport? transport = IGPRealtimeTransport.Kcp,
            object? details = null)
        {
            if (realtimeConnectionState == IGPRealtimeConnectionState.Failed &&
                dataPlaneTransport == null &&
                currentDataPlaneMode == IGPDataPlaneMode.None)
            {
                return new IGPSDKException(
                    string.IsNullOrWhiteSpace(currentHostedDataPlaneError)
                        ? errorMessage
                        : currentHostedDataPlaneError);
            }

            bool failureAlreadyReported = realtimeConnectionState == IGPRealtimeConnectionState.Failed;
            InvalidateRealtimeDataPlane(
                "interrupted",
                errorMessage,
                IGPRealtimeConnectionState.Failed);
            reconnectAttempt = 1;
            nextDataPlaneAttemptUtc = DateTime.UtcNow + ReconnectDelays[0];

            if (!isDestroyed && !failureAlreadyReported)
            {
                LogSdkError("hosted-data-plane", $"event=interrupted error={FormatNetworkLogValue(errorMessage)}");
                PublishMultiplayerError(
                    errorCode,
                    errorMessage,
                    "hosted-data-plane",
                    transport,
                    true,
                    details);
            }

            return new IGPSDKException(errorMessage);
        }

        private void HandleKcpConnectionStateChanged(bool connected)
        {
            if (connected)
            {
                if (dataPlaneTransport == null || currentDataPlaneMode == IGPDataPlaneMode.None)
                {
                    return;
                }

                currentHostedDataPlaneStatus = "connected";
                currentHostedDataPlaneError = string.Empty;
                UpdateRealtimeConnectionState(IGPRealtimeConnectionState.Ready);
                return;
            }

            if (suppressKcpDisconnectHandling)
            {
                return;
            }

            if (dataPlaneTransport != null && currentDataPlaneMode != IGPDataPlaneMode.None)
            {
                _ = MarkHostedDataPlaneInterrupted("KCP connection dropped");
            }
        }

        private async Task RequestAndAttachHostedDataPlaneAsync(
            string roomId,
            string attachPlayerId,
            int roomGeneration)
        {
            var session = EnsureHostedSession();
            Exception? lastError = null;
            for (var attempt = 1; attempt <= HostedDataPlaneAttachAttempts; attempt += 1)
            {
                if (roomGeneration != realtimeRoomGeneration)
                {
                    throw new IGPSDKException("Realtime data-plane connection was superseded by a room change");
                }

                var attemptId = ++hostedDataPlaneAttemptId;
                UpdateRealtimeConnectionState(IGPRealtimeConnectionState.RequestingDescriptor);
                currentHostedDataPlaneStatus = attempt == 1 ? "kcp-token-requesting" : $"kcp-retry-token-requesting:{attempt}";
                currentHostedDataPlaneError = string.Empty;
                LogHostedDataPlaneEvent("request-token", attemptId);

                try
                {
                    var dataPlaneResponse = await session.RequestDataPlaneAsync(RuntimeCancellationToken);
                    ThrowIfDataPlaneConnectionIsStale(roomGeneration);
                    currentHostedDataPlaneStatus = $"descriptor-received:{attempt}";
                    await AttachHostedDataPlaneAsync(
                        roomId,
                        attachPlayerId,
                        dataPlaneResponse,
                        attemptId,
                        roomGeneration);
                    await WaitForHostedDataPlaneReadyAsync(
                        HostedDataPlaneReadyTimeout,
                        attemptId,
                        roomGeneration);
                    LogHostedDataPlaneEvent("handshake-ack", attemptId);
                    return;
                }
                catch (StaleHostedDataPlaneAttemptException)
                {
                    LogHostedDataPlaneEvent("stale-attempt-discarded", attemptId);
                    throw new IGPSDKException("Realtime data-plane connection was superseded by a room change");
                }
                catch (OperationCanceledException)
                {
                    ThrowIfDataPlaneConnectionIsStale(roomGeneration);
                    const string error = "Hosted data plane request was canceled";
                    currentHostedDataPlaneStatus = "failed:canceled";
                    currentHostedDataPlaneError = error;
                    UpdateRealtimeConnectionState(IGPRealtimeConnectionState.Failed);
                    LogSdkError("hosted-data-plane", $"event=request-canceled error={FormatNetworkLogValue(error)}");
                    PublishMultiplayerError(
                        "HOSTED_DATA_PLANE_CANCELED",
                        error,
                        "hosted-data-plane",
                        IGPRealtimeTransport.Kcp,
                        true);
                    throw;
                }
                catch (TimeoutException ex)
                {
                    ThrowIfDataPlaneConnectionIsStale(roomGeneration);
                    lastError = ex;
                    ResetHostedDataPlaneState($"retrying:{attempt}", ex.Message);
                    if (attempt >= HostedDataPlaneAttachAttempts)
                    {
                        currentHostedDataPlaneStatus = "failed:kcp-handshake-timeout";
                        currentHostedDataPlaneError = ex.Message;
                        UpdateRealtimeConnectionState(IGPRealtimeConnectionState.Failed);
                        LogSdkError(
                            "hosted-data-plane",
                            $"event=attach-failed class=KcpHandshakeTimeout error={FormatNetworkLogValue(ex.Message)}");
                        throw;
                    }

                    LogHostedDataPlaneEvent("retry-scheduled", attemptId, "KcpHandshakeTimeout", ex.Message);
                    await DelayBeforeHostedDataPlaneRetryAsync(attempt, roomGeneration);
                }
                catch (Exception ex)
                {
                    ThrowIfDataPlaneConnectionIsStale(roomGeneration);
                    lastError = ex;
                    ResetHostedDataPlaneState($"retrying:{attempt}", ex.Message);
                    if (attempt >= HostedDataPlaneAttachAttempts)
                    {
                        currentHostedDataPlaneStatus = "failed";
                        currentHostedDataPlaneError = ex.Message;
                        UpdateRealtimeConnectionState(IGPRealtimeConnectionState.Failed);
                        LogSdkError(
                            "hosted-data-plane",
                            $"event=attach-failed class={ClassifyHostedDataPlaneException(ex)} error={FormatNetworkLogValue(ex.Message)}");
                        throw;
                    }

                    LogHostedDataPlaneEvent("retry-scheduled", attemptId, ClassifyHostedDataPlaneException(ex), ex.Message);
                    await DelayBeforeHostedDataPlaneRetryAsync(attempt, roomGeneration);
                }
            }

            throw lastError ?? new IGPSDKException("Hosted data plane attach failed");
        }

        private async Task DelayBeforeHostedDataPlaneRetryAsync(
            int completedAttempt,
            int roomGeneration)
        {
            ThrowIfDataPlaneConnectionIsStale(roomGeneration);
            var delayIndex = Math.Min(completedAttempt - 1, HostedDataPlaneRetryDelays.Length - 1);
            var delay = HostedDataPlaneRetryDelays[delayIndex];
            UpdateRealtimeConnectionState(IGPRealtimeConnectionState.RequestingDescriptor);
            await Task.Delay(delay, RuntimeCancellationToken);
        }

        private void ThrowIfDataPlaneConnectionIsStale(int roomGeneration)
        {
            if (roomGeneration != realtimeRoomGeneration)
            {
                throw new StaleHostedDataPlaneAttemptException();
            }
        }

        private string ClassifyHostedDataPlaneException(Exception ex)
        {
            if (ex is TimeoutException)
            {
                return "KcpHandshakeTimeout";
            }

            if (ex is SocketException)
            {
                return "KcpSocketInitFailed";
            }

            if (ex is IGPSDKException sdkException)
            {
                if (string.Equals(sdkException.ErrorCode, IGPMultiplayerErrorCodes.ERR_UNAUTHORIZED, StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(sdkException.ErrorCode, IGPMultiplayerErrorCodes.ERR_FORBIDDEN, StringComparison.OrdinalIgnoreCase))
                {
                    return "KcpTokenInvalid";
                }
            }

            return "HostedDataPlaneAttachFailed";
        }

        private Task AttachHostedDataPlaneAsync(
            string roomId,
            string attachPlayerId,
            IGPHostSessionDataPlaneResponse dataPlaneResponse,
            int attemptId,
            int roomGeneration)
        {
            ThrowIfDataPlaneConnectionIsStale(roomGeneration);
            if (dataPlaneResponse.DataPlane == null)
            {
                currentHostedDataPlaneStatus = "failed:missing-descriptor";
                throw new IGPSDKException("Missing hosted data plane descriptor");
            }

            ApplyNegotiatedTransportOptions(
                dataPlaneResponse.DataPlane.reliableMessageMaxBytes,
                dataPlaneResponse.DataPlane.reliableChunkMaxBytes,
                dataPlaneResponse.DataPlane.kcpDataPlanePayloadMaxBytes,
                dataPlaneResponse.DataPlane.kcpFrameMaxBytes);

            switch (dataPlaneResponse.DataPlane.Mode)
            {
                case IGPHostedDataPlaneMode.DirectKcp:
                    AttachDirectKcpDataPlane(roomId, attachPlayerId, dataPlaneResponse.DataPlane, attemptId);
                    return Task.CompletedTask;
                default:
                    currentHostedDataPlaneStatus = $"failed:unsupported-mode:{dataPlaneResponse.DataPlane.Mode}";
                    throw new IGPSDKException($"Unsupported hosted data plane mode: {dataPlaneResponse.DataPlane.Mode}");
            }
        }

        private void AttachDirectKcpDataPlane(
            string roomId,
            string attachPlayerId,
            IGPHostSessionDataPlaneDescriptor descriptor,
            int attemptId)
        {
            if (KcpClient == null)
            {
                throw new IGPSDKException("IGP KCP client is not initialized");
            }

            dataPlaneTransport?.Disconnect();
            UnreliableUdpClient?.Disconnect();
            dataPlaneTransport = IGPDataPlaneTransportFactory.Create(descriptor, KcpClient);
            dataPlaneTransport.Connect(descriptor, roomId, attachPlayerId);
            AttachUnreliableUdpLane(descriptor, roomId, attachPlayerId);
            currentDataPlaneMode = dataPlaneTransport.Mode;
            currentHostedDataPlaneHost = descriptor.Host ?? string.Empty;
            currentHostedDataPlanePort = descriptor.Port;
            currentHostedDataPlaneExpiresAtUnixMs = descriptor.ExpiresAtUnixMs;
            currentHostedDataPlaneRoomId = roomId;
            currentHostedDataPlanePlayerId = attachPlayerId;
            currentHostedDataPlaneStatus = "kcp-handshaking";
            currentHostedDataPlaneError = string.Empty;
            UpdateRealtimeConnectionState(IGPRealtimeConnectionState.Connecting);

            LogHostedDataPlaneEvent("handshake-sent", attemptId);
        }

        private void AttachUnreliableUdpLane(
            IGPHostSessionDataPlaneDescriptor descriptor,
            string roomId,
            string attachPlayerId)
        {
            var client = UnreliableUdpClient;
            if (client == null ||
                string.IsNullOrWhiteSpace(descriptor.unreliableUdpHost) ||
                descriptor.unreliableUdpPort == 0 ||
                string.IsNullOrWhiteSpace(descriptor.unreliableUdpToken) ||
                !descriptor.unreliableUdpPayloadMaxBytes.HasValue ||
                descriptor.unreliableUdpPayloadMaxBytes.Value <= 0)
            {
                return;
            }

            client.Connect(
                descriptor.unreliableUdpHost,
                (int)descriptor.unreliableUdpPort,
                roomId,
                attachPlayerId,
                descriptor.unreliableUdpToken,
                descriptor.unreliableUdpPayloadMaxBytes.Value);
        }

        private async Task WaitForHostedDataPlaneReadyAsync(
            TimeSpan timeout,
            int attemptId,
            int roomGeneration)
        {
            var deadline = DateTime.UtcNow.Add(timeout);
            while (DateTime.UtcNow < deadline)
            {
                if (attemptId != hostedDataPlaneAttemptId || roomGeneration != realtimeRoomGeneration)
                {
                    throw new StaleHostedDataPlaneAttemptException();
                }

                if (IsKcpConnected)
                {
                    currentHostedDataPlaneStatus = "connected";
                    currentHostedDataPlaneError = string.Empty;
                    UpdateRealtimeConnectionState(IGPRealtimeConnectionState.Ready);
                    return;
                }

                if (string.Equals(currentHostedDataPlaneStatus, "error", StringComparison.OrdinalIgnoreCase) &&
                    !string.IsNullOrWhiteSpace(currentHostedDataPlaneError))
                {
                    throw new IGPSDKException(currentHostedDataPlaneError);
                }

                await Task.Delay(100, RuntimeCancellationToken);
            }

            throw new TimeoutException(BuildHostedDataPlaneTimeoutMessage(timeout));
        }

        private string BuildHostedDataPlaneTimeoutMessage(TimeSpan timeout)
        {
            var target = string.IsNullOrWhiteSpace(currentHostedDataPlaneHost) || currentHostedDataPlanePort == 0
                ? "the requested direct data endpoint"
                : $"{currentHostedDataPlaneHost}:{currentHostedDataPlanePort}";

            return
                $"Hosted data plane connection was not established within {timeout.TotalSeconds:F0}s; " +
                $"no response was received from {target}";
        }

        private void ResetHostedDataPlaneState(string status = "idle", string error = "")
        {
            suppressKcpDisconnectHandling = true;
            try
            {
                dataPlaneTransport?.Disconnect();
                UnreliableUdpClient?.Disconnect();
            }
            finally
            {
                suppressKcpDisconnectHandling = false;
            }
            dataPlaneTransport = null;
            currentDataPlaneMode = IGPDataPlaneMode.None;
            currentHostedDataPlaneHost = string.Empty;
            currentHostedDataPlanePort = 0;
            currentHostedDataPlaneExpiresAtUnixMs = 0;
            currentHostedDataPlaneRoomId = string.Empty;
            currentHostedDataPlanePlayerId = string.Empty;
            currentHostedDataPlaneStatus = status;
            currentHostedDataPlaneError = error;
        }

        private void InvalidateRealtimeDataPlane(
            string status,
            string error = "",
            IGPRealtimeConnectionState state = IGPRealtimeConnectionState.Idle)
        {
            realtimeRoomGeneration += 1;
            hostedDataPlaneAttemptId += 1;

            ResetHostedDataPlaneState(status, error);
            UpdateRealtimeConnectionState(state);
        }

        private void ApplyNegotiatedTransportOptions(
            int? reliableMessageMaxBytes,
            int? reliableChunkMaxBytes,
            int? kcpDataPlanePayloadMaxBytes,
            int? kcpFrameMaxBytes)
        {
            var negotiated = IGPReliableTransportOptionsNegotiation.Resolve(
                reliableMessageMaxBytes,
                reliableChunkMaxBytes,
                kcpDataPlanePayloadMaxBytes,
                kcpFrameMaxBytes);

            KcpClient?.ApplyTransportOptions(negotiated);
            Network?.ApplyTransportOptions(negotiated);
        }
    }
}
