#nullable enable
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;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
using UnityEngine.Events;
#if UNITY_EDITOR
using UnityEditor;
#endif
using IGP.UnitySDK.Abstractions;
using IGP.UnitySDK.Models;
using IGP.UnitySDK.Core;
using IGP.UnitySDK.Network;
using IGP.UnitySDK.Protocol;

namespace IGP.UnitySDK
{
    public partial class IGPRuntimeManager
    {

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

            currentHostedDataPlaneStatus = IsKcpConnected ? "connected" : "connecting";
            currentHostedDataPlaneError = string.Empty;
            UpdateHostedConnectionState(IsKcpConnected
                ? IGPHostedConnectionState.KcpConnected
                : IGPHostedConnectionState.KcpHandshaking);
        }

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

        /// <summary>
        /// 确保宿主数据面已附着；如果尚未附着，则主动请求一次。
        /// </summary>
        public async Task EnsureHostedDataPlaneAttachedAsync()
        {
            EnsureSDKInitialized();

            LogSdkInfo(
                "hosted-data-plane",
                $"event=ensure-attached mode={currentDataPlaneMode} " +
                $"transportAttached={dataPlaneTransport != null} hostedSession={IsHostedSessionAttached} " +
                $"realtimeReady={WebSocketClient?.IsConnected ?? false} roomId={currentRoomId}");

            if (IsKcpConnected && currentDataPlaneMode != IGPDataPlaneMode.None && dataPlaneTransport != null)
            {
                LogSdkInfo("hosted-data-plane", "event=ensure-attached-skipped reason=already-attached");
                return;
            }

            if (WebSocketClient == null)
            {
                throw new IGPSDKException("Hosted realtime client is not initialized");
            }

            if (!IsHostedSessionAttached)
            {
                throw new IGPSDKException("Room context is required before hosted data plane can be used");
            }

            if (!WebSocketClient.IsConnected)
            {
                throw new IGPSDKException("Hosted realtime connection is not ready");
            }

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

            await RequestAndAttachHostedDataPlaneAsync(currentRoomId);
        }


        /// <summary>
        /// Internal helper to send P2P messages using the best available transport.
        /// </summary>
        internal bool TrySendP2PMessage(Message msg)
        {
            var transport = dataPlaneTransport;
            if (IsKcpConnected && transport != null)
            {
                return transport.TrySendMessage(msg);
            }

            throw MarkHostedDataPlaneInterrupted(
                string.IsNullOrWhiteSpace(currentHostedDataPlaneError)
                    ? "Connection interrupted: direct data connection is unavailable"
                    : currentHostedDataPlaneError);
        }

        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 void SendP2PMessage(Message msg)
        {
            if (!TrySendP2PMessage(msg))
            {
                throw new IGPSDKException("Direct data connection failed to queue the message");
            }
        }


        private IGPSDKException MarkHostedDataPlaneInterrupted(string errorMessage)
        {
            if (hostedConnectionState == IGPHostedConnectionState.ConnectionFailed &&
                dataPlaneTransport == null &&
                currentDataPlaneMode == IGPDataPlaneMode.None)
            {
                return new IGPSDKException(
                    string.IsNullOrWhiteSpace(currentHostedDataPlaneError)
                        ? errorMessage
                        : currentHostedDataPlaneError);
            }

            bool failureAlreadyReported = hostedConnectionState == IGPHostedConnectionState.ConnectionFailed;
            ResetHostedDataPlaneState("interrupted", errorMessage);
            UpdateHostedConnectionState(IGPHostedConnectionState.ConnectionFailed);

            if (!isDestroyed && !failureAlreadyReported)
            {
                LogSdkError("hosted-data-plane", $"event=interrupted error={FormatNetworkLogValue(errorMessage)}");
                onError?.Invoke(errorMessage);
            }

            return new IGPSDKException(errorMessage);
        }

        private void HandleKcpConnectionStateChanged(bool connected)
        {
            if (connected)
            {
                currentHostedDataPlaneStatus = "connected";
                currentHostedDataPlaneError = string.Empty;
                UpdateHostedConnectionState(IGPHostedConnectionState.KcpConnected);
                return;
            }

            if (suppressKcpDisconnectHandling)
            {
                return;
            }

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

        private void HandleKcpTransportFaulted(Core.IGPKcpFaultReason reason)
        {
            if (suppressKcpDisconnectHandling)
            {
                return;
            }

            _ = MarkHostedDataPlaneInterrupted($"KCP transport fault: {reason}");
        }


        private async Task RequestAndAttachHostedDataPlaneAsync(string roomId)
        {
            if (WebSocketClient == null)
            {
                currentHostedDataPlaneStatus = "skipped:no-hosted-realtime";
                currentHostedDataPlaneError = "Hosted realtime client is not initialized";
                UpdateHostedConnectionState(IGPHostedConnectionState.ConnectionFailed);
                return;
            }

            if (KcpClient == null)
            {
                currentHostedDataPlaneStatus = "skipped:no-kcp-client";
                currentHostedDataPlaneError = "KCP client is not initialized";
                UpdateHostedConnectionState(IGPHostedConnectionState.ConnectionFailed);
                return;
            }

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

            Task attachTask;
            var now = DateTime.UtcNow;
            lock (hostedDataPlaneAttachLock)
            {
                if (IsHostedDataPlaneActiveFor(roomId, playerId))
                {
                    LogHostedDataPlaneEvent("single-flight-existing", detail: "reason=active-session");
                    return;
                }

                if (hostedDataPlaneAttachTask != null &&
                    !hostedDataPlaneAttachTask.IsCompleted &&
                    string.Equals(hostedDataPlaneAttachRoomId, roomId, StringComparison.Ordinal) &&
                    string.Equals(hostedDataPlaneAttachPlayerId, playerId, StringComparison.Ordinal))
                {
                    LogHostedDataPlaneEvent("single-flight-join", detail: "reason=in-flight");
                    attachTask = hostedDataPlaneAttachTask;
                }
                else
                {
                    if (string.Equals(hostedDataPlaneAttachRoomId, roomId, StringComparison.Ordinal) &&
                        string.Equals(hostedDataPlaneAttachPlayerId, playerId, StringComparison.Ordinal) &&
                        now - hostedDataPlaneLastAttachStartedAtUtc < HostedDataPlaneDebounceWindow)
                    {
                        var remaining = HostedDataPlaneDebounceWindow - (now - hostedDataPlaneLastAttachStartedAtUtc);
                        currentHostedDataPlaneStatus = "debounced";
                        currentHostedDataPlaneError =
                            $"Hosted data plane connect is debounced for {remaining.TotalSeconds:F1}s";
                        LogHostedDataPlaneEvent("single-flight-debounced", detail: $"remainingMs={(int)remaining.TotalMilliseconds}");
                        throw new IGPSDKException(currentHostedDataPlaneError);
                    }

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

                    hostedDataPlaneAttachRoomId = roomId;
                    hostedDataPlaneAttachPlayerId = playerId;
                    hostedDataPlaneLastAttachStartedAtUtc = now;
                    attachTask = RequestAndAttachHostedDataPlaneCoreAsync(roomId, playerId);
                    hostedDataPlaneAttachTask = attachTask;
                }
            }

            await attachTask;
        }

        private bool IsHostedDataPlaneActiveFor(string roomId, string currentPlayerId)
        {
            if (dataPlaneTransport == null || currentDataPlaneMode == IGPDataPlaneMode.None)
            {
                return false;
            }

            if (!string.Equals(currentHostedDataPlaneRoomId, roomId, StringComparison.Ordinal) ||
                !string.Equals(currentHostedDataPlanePlayerId, currentPlayerId, StringComparison.Ordinal))
            {
                return false;
            }

            return IsKcpConnected ||
                   hostedConnectionState == IGPHostedConnectionState.KcpTokenRequesting ||
                   hostedConnectionState == IGPHostedConnectionState.KcpHandshaking ||
                   hostedConnectionState == IGPHostedConnectionState.KcpConnected;
        }

        private async Task RequestAndAttachHostedDataPlaneCoreAsync(string roomId, string attachPlayerId)
        {
            var session = EnsureHostedSession();
            Exception? lastError = null;
            for (var attempt = 1; attempt <= HostedDataPlaneAttachAttempts; attempt += 1)
            {
                var attemptId = ++hostedDataPlaneAttemptId;
                UpdateHostedConnectionState(IGPHostedConnectionState.KcpTokenRequesting);
                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);
                    currentHostedDataPlaneStatus = $"descriptor-received:{attempt}";
                    await AttachHostedDataPlaneAsync(roomId, attachPlayerId, dataPlaneResponse, attemptId);
                    await WaitForHostedDataPlaneReadyAsync(HostedDataPlaneReadyTimeout, attemptId);
                    LogHostedDataPlaneEvent("handshake-ack", attemptId);
                    return;
                }
                catch (StaleHostedDataPlaneAttemptException)
                {
                    LogHostedDataPlaneEvent("stale-attempt-discarded", attemptId);
                    return;
                }
                catch (OperationCanceledException)
                {
                    const string error = "Hosted data plane request was canceled";
                    currentHostedDataPlaneStatus = "failed:canceled";
                    currentHostedDataPlaneError = error;
                    UpdateHostedConnectionState(IGPHostedConnectionState.ConnectionFailed);
                    LogSdkError("hosted-data-plane", $"event=request-canceled error={FormatNetworkLogValue(error)}");
                    onError?.Invoke(error);
                    throw;
                }
                catch (TimeoutException ex)
                {
                    lastError = ex;
                    ResetHostedDataPlaneState($"retrying:{attempt}", ex.Message);
                    if (attempt >= HostedDataPlaneAttachAttempts)
                    {
                        currentHostedDataPlaneStatus = "failed:kcp-handshake-timeout";
                        currentHostedDataPlaneError = ex.Message;
                        UpdateHostedConnectionState(IGPHostedConnectionState.ConnectionFailed);
                        LogSdkError(
                            "hosted-data-plane",
                            $"event=attach-failed class=KcpHandshakeTimeout error={FormatNetworkLogValue(ex.Message)}");
                        throw;
                    }

                    LogHostedDataPlaneEvent("retry-scheduled", attemptId, "KcpHandshakeTimeout", ex.Message);
                    await DelayBeforeHostedDataPlaneRetryAsync(attempt);
                }
                catch (Exception ex)
                {
                    lastError = ex;
                    ResetHostedDataPlaneState($"retrying:{attempt}", ex.Message);
                    if (attempt >= HostedDataPlaneAttachAttempts)
                    {
                        currentHostedDataPlaneStatus = "failed";
                        currentHostedDataPlaneError = ex.Message;
                        UpdateHostedConnectionState(IGPHostedConnectionState.ConnectionFailed);
                        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);
                }
            }

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

        private async Task DelayBeforeHostedDataPlaneRetryAsync(int completedAttempt)
        {
            var delayIndex = Math.Min(completedAttempt - 1, HostedDataPlaneRetryDelays.Length - 1);
            var delay = HostedDataPlaneRetryDelays[delayIndex];
            UpdateHostedConnectionState(IGPHostedConnectionState.KcpTokenRequesting);
            await Task.Delay(delay, RuntimeCancellationToken);
        }

        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, IGPErrorCodes.ERR_UNAUTHORIZED, StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(sdkException.ErrorCode, IGPErrorCodes.ERR_FORBIDDEN, StringComparison.OrdinalIgnoreCase))
                {
                    return "KcpTokenInvalid";
                }
            }

            return "HostedDataPlaneAttachFailed";
        }

        private Task AttachHostedDataPlaneAsync(
            string roomId,
            string attachPlayerId,
            IGPHostSessionDataPlaneResponse dataPlaneResponse,
            int attemptId)
        {
            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;
            UpdateHostedConnectionState(IGPHostedConnectionState.KcpHandshaking);

            LogHostedDataPlaneEvent("handshake-sent", attemptId);

            if (enableKcpHeartbeat)
            {
                dataPlaneTransport.StartHeartbeat(kcpHeartbeatInterval);
                LogSdkInfo("heartbeat", $"event=started intervalSeconds={kcpHeartbeatInterval:F2}");
            }
        }

        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)
        {
            var deadline = DateTime.UtcNow.Add(timeout);
            while (DateTime.UtcNow < deadline)
            {
                if (attemptId != hostedDataPlaneAttemptId)
                {
                    throw new StaleHostedDataPlaneAttemptException();
                }

                if (IsKcpConnected)
                {
                    currentHostedDataPlaneStatus = "connected";
                    currentHostedDataPlaneError = string.Empty;
                    UpdateHostedConnectionState(IGPHostedConnectionState.KcpConnected);
                    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;
            if (string.Equals(status, "idle", StringComparison.Ordinal) ||
                status.StartsWith("idle:", StringComparison.Ordinal))
            {
                UpdateHostedConnectionState(IsWebSocketConnected
                    ? IGPHostedConnectionState.WebSocketConnected
                    : IGPHostedConnectionState.Disconnected);
            }
        }

        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);
        }
    }
}
