#nullable enable
using IGP.UnitySDK.Models;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using IGP.Multiplayer.Core;
using IGP.Multiplayer.Models;
using IGP.Multiplayer.Network;
using IGP.UnitySDK;
using IGP.UnitySDK.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace IGP.Multiplayer
{
    public enum IGPDataPlaneMode
    {
        None = 0,
        DirectKcp = 1,
    }

    public enum IGPRealtimeConnectionState
    {
        Idle = 0,
        WaitingForRoom = 1,
        RequestingDescriptor = 2,
        Connecting = 3,
        Ready = 4,
        Failed = 5,
    }

    internal interface IIGPMultiplayerTransportBackend
    {
        bool IsReady { get; }
        event Action<IGPRealtimeConnectionState>? ConnectionChanged;
        event Action<IGPDataReceived>? DataReceived;
        event Action<IGPPeerActivity>? PeerActivity;
    }

    [RequireComponent(typeof(IGPRuntimeManager))]
    public partial class IGPMultiplayerRuntime : MonoBehaviour,
        IIGPRuntimeExtension,
        IIGPMultiplayerTransportBackend
    {
        private static readonly TimeSpan HostedDataPlaneReadyTimeout = TimeSpan.FromSeconds(5);
        private static readonly TimeSpan[] ReconnectDelays =
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(2),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10),
            TimeSpan.FromSeconds(30),
        };
        private const int HostedDataPlaneAttachAttempts = 1;
        private static readonly TimeSpan[] HostedDataPlaneRetryDelays = { TimeSpan.Zero };
        private const float RttStatsOverlayRefreshIntervalSeconds = 0.5f;
        private const float RttStatsOverlayQueueAverageWindowSeconds = 1.0f;
        private const float RttStatsOverlayQueuePeakWindowSeconds = 5.0f;

        [Header("Core Runtime")]
        [SerializeField] private IGPRuntimeManager? coreRuntime;

        [Header("KCP Transport Settings")]
        [Min(1)] [SerializeField] private int kcpMaxDatagramsPerTick = IGPKcpClient.DefaultMaxDatagramsPerTick;
        [Min(IGPKcpClient.MinWindowSize)] [SerializeField] private int kcpSendWindowSize = IGPKcpClient.DefaultWindowSize;
        [Min(IGPKcpClient.MinWindowSize)] [SerializeField] private int kcpReceiveWindowSize = IGPKcpClient.DefaultWindowSize;

        [Header("Diagnostics")]
        [SerializeField] private bool showNetworkDiagnostics;

        public event Action<IGPMultiplayerConnectionChangedEvent>? ConnectionChanged;
        public event Action<IGPRoomChangedEvent>? RoomChanged;
        public event Action<IGPMessageReceivedEvent>? MessageReceived;
        public event Action<IGPMultiplayerErrorEvent>? ErrorOccurred;

        private event Action<IGPRealtimeConnectionState>? backendConnectionChanged;
        private event Action<IGPDataReceived>? backendDataReceived;
        private event Action<IGPPeerActivity>? backendPeerActivity;

        [NonSerialized] public string playerColor = "#FF0000";
        [NonSerialized] public string playerAvatar = "default";
        [NonSerialized] public int playerAvatarIndex;

        private IIGPRuntimeContext? runtimeContext;
        private IIGPDesktopNotificationSource? desktopNotificationSource;
        private CancellationTokenSource? cancellationTokenSource;
        private bool isDestroyed;
        private bool initializeRequested;
        private bool sdkInitialized;
        private Task<bool>? sdkInitializationTask;
        private Task? shutdownTask;
        private IGPLaunchOptions? pendingLaunchOptions;
        private IGPHostSessionClient? hostSessionClient;
        private string serverUrl = string.Empty;
        private string playerId = string.Empty;
        private string playerName = string.Empty;
        private string currentRoomId = string.Empty;
        private string currentRoomCode = string.Empty;
        private string sessionToken = string.Empty;
        private Room roomData = new Room();
        private Player currentPlayerData = new Player();
        private string hostedConnectionStatus = "disconnected";
        private IGPRealtimeConnectionState realtimeConnectionState = IGPRealtimeConnectionState.Idle;
        private string currentHostedSessionEndpoint = string.Empty;
        private string currentHostedSessionSecret = string.Empty;
        private IGPDataPlaneMode currentDataPlaneMode = IGPDataPlaneMode.None;
        private string currentHostedDataPlaneHost = string.Empty;
        private uint currentHostedDataPlanePort;
        private ulong currentHostedDataPlaneExpiresAtUnixMs;
        private string currentHostedDataPlaneRoomId = string.Empty;
        private string currentHostedDataPlanePlayerId = string.Empty;
        private string currentHostedDataPlaneStatus = "idle";
        private string currentHostedDataPlaneError = string.Empty;
        private IIGPDataPlaneTransport? dataPlaneTransport;
        private int realtimeRoomGeneration;
        private int hostedDataPlaneAttemptId;
        private bool hostedDisconnectRequested;
        private bool hostedBootstrapRequestInFlight;
        private bool suppressKcpDisconnectHandling;
        private Task? automaticDataPlaneTask;
        private DateTime nextDataPlaneAttemptUtc = DateTime.MinValue;
        private int reconnectAttempt;
        private bool terminalDataPlaneFailure;
        private IGPLogLevel appliedLogLevel = IGPLogLevel.Off;

        private bool hasRttStatsOverlaySnapshot;
        private IGPKcpTransportStats rttStatsOverlaySnapshot;
        private float nextRttStatsOverlayRefreshAt;
        private bool hasRttStatsOverlayQueueAverage;
        private int rttStatsOverlayQueueAverageWaitSnd;
        private int rttStatsOverlayQueueAverageSndQueue;
        private int rttStatsOverlayQueueAverageSndBuf;
        private bool hasRttStatsOverlayQueuePeak;
        private int rttStatsOverlayQueuePeakWaitSnd;
        private int rttStatsOverlayQueuePeakSndQueue;
        private int rttStatsOverlayQueuePeakSndBuf;
        private readonly Queue<RttStatsOverlayQueueSample> rttStatsOverlayQueueSamples = new Queue<RttStatsOverlayQueueSample>();
        private float rttStatsOverlayQueueLastSampleAt = -1f;

        public int InitializationOrder => 100;
        public bool IsInitialized => sdkInitialized;
        public bool IsReady => IsRealtimeReady;
        public IGPConfig? Config => runtimeContext?.Config;
        public IGPLogLevel LogLevel => Config?.debugLogging ?? IGPLogLevel.Off;
        private CancellationToken RuntimeCancellationToken => cancellationTokenSource?.Token ?? CancellationToken.None;

        private IGPKcpClient? KcpClient { get; set; }
        private IGPUnreliableUdpClient? UnreliableUdpClient { get; set; }
        public IGPNetwork Network { get; private set; } = null!;

        event Action<IGPRealtimeConnectionState>? IIGPMultiplayerTransportBackend.ConnectionChanged
        {
            add => backendConnectionChanged += value;
            remove => backendConnectionChanged -= value;
        }

        event Action<IGPDataReceived>? IIGPMultiplayerTransportBackend.DataReceived
        {
            add => backendDataReceived += value;
            remove => backendDataReceived -= value;
        }

        event Action<IGPPeerActivity>? IIGPMultiplayerTransportBackend.PeerActivity
        {
            add => backendPeerActivity += value;
            remove => backendPeerActivity -= value;
        }
        public string ServerUrl { get => serverUrl; set => serverUrl = value ?? string.Empty; }
        public string CurrentRoomId => currentRoomId;
        public string CurrentRoomCode => currentRoomCode;
        public string CurrentSceneGate => roomData.sceneGate ?? string.Empty;
        public Room CurrentRoomData => roomData;
        public Player CurrentPlayerData => currentPlayerData;
        public string SessionToken { get => sessionToken; set => sessionToken = value ?? string.Empty; }
        public string PlayerId { get => playerId; set => playerId = value ?? string.Empty; }
        public string PlayerName { get => playerName; set => playerName = value ?? string.Empty; }
        public bool IsHostedSessionAttached => hostSessionClient?.IsAttached ?? false;
        public string CurrentHostedSessionEndpoint => currentHostedSessionEndpoint;
        public bool IsKcpConnected => dataPlaneTransport?.IsConnected ?? (KcpClient?.IsConnected ?? false);
        public bool IsUnreliableUdpConnected => UnreliableUdpClient?.IsConnected ?? false;
        public int UnreliableUdpPayloadMaxBytes => UnreliableUdpClient?.PayloadMaxBytes ?? IGPUnreliableUdpClient.DefaultPayloadMaxBytes;
        public IGPDataPlaneMode CurrentDataPlaneMode => currentDataPlaneMode;
        public bool IsRealtimeReady => realtimeConnectionState == IGPRealtimeConnectionState.Ready && IsKcpConnected && dataPlaneTransport != null;
        public string CurrentHostedDataPlaneHost => currentHostedDataPlaneHost;
        public uint CurrentHostedDataPlanePort => currentHostedDataPlanePort;
        public string CurrentHostedDataPlaneStatus => currentHostedDataPlaneStatus;
        public string CurrentHostedDataPlaneError => currentHostedDataPlaneError;
        public IGPRealtimeConnectionState RealtimeConnectionState => realtimeConnectionState;
        public IGPRTTStats? KcpRTTStats => dataPlaneTransport?.RTTStats ?? KcpClient?.RTTStats;
        public IGPRTTStats? AppRTTStats => dataPlaneTransport?.RTTStats ?? KcpClient?.AppRTTStats;
        public IGPKcpTransportStats KcpTransportStats => dataPlaneTransport?.TransportStats ?? KcpClient?.TransportStats ?? IGPKcpTransportStats.Unavailable;
        public IGPKcpHealthState KcpHealthState => KcpClient?.HealthState ?? IGPKcpHealthState.Disconnected;
        public IGPKcpFaultReason KcpLastFaultReason => KcpClient?.LastFaultReason ?? IGPKcpFaultReason.None;
        public bool IsKcpAlive => dataPlaneTransport?.IsAlive ?? (KcpClient?.IsAlive ?? false);

        internal void ObserveMirrorApplicationFrameCreated(int payloadCount, int rawBytes, int wireBytes, bool compressed)
        {
            KcpClient?.ObserveApplicationFrameCreated(payloadCount, rawBytes, wireBytes, compressed);
        }

        internal void ObserveMirrorApplicationFrameReceived(int payloadCount)
        {
            KcpClient?.ObserveApplicationFrameReceived(payloadCount);
        }

        internal void ObserveMirrorApplicationFrameQueue(int messages, int bytes, double oldestAgeSeconds)
        {
            KcpClient?.ObserveApplicationFrameQueue(messages, bytes, oldestAgeSeconds);
        }

        internal void ObserveMirrorApplicationFrameSchedulerGap(double gapSeconds)
        {
            KcpClient?.ObserveApplicationFrameSchedulerGap(gapSeconds);
        }
        public bool ShowNetworkDiagnostics => showNetworkDiagnostics;

        public int KcpMaxDatagramsPerTick
        {
            get => NormalizeKcpMaxDatagramsPerTick(kcpMaxDatagramsPerTick);
            set { kcpMaxDatagramsPerTick = NormalizeKcpMaxDatagramsPerTick(value); ApplyKcpRuntimeSettings(); }
        }

        public int KcpSendWindowSize
        {
            get => NormalizeKcpWindowSize(kcpSendWindowSize);
            set { kcpSendWindowSize = NormalizeKcpWindowSize(value); ApplyKcpRuntimeSettings(); }
        }

        public int KcpReceiveWindowSize
        {
            get => NormalizeKcpWindowSize(kcpReceiveWindowSize);
            set { kcpReceiveWindowSize = NormalizeKcpWindowSize(value); ApplyKcpRuntimeSettings(); }
        }

        private void Awake()
        {
            coreRuntime ??= GetComponent<IGPRuntimeManager>();
        }

        public async Task<bool> InitializeAsync(IIGPRuntimeContext context, CancellationToken cancellationToken = default)
        {
            if (isDestroyed || shutdownTask != null) return false;
            if (sdkInitializationTask != null) return await sdkInitializationTask;
            sdkInitializationTask = InitializeInternalAsync(context, cancellationToken);
            return await sdkInitializationTask;
        }

        private async Task<bool> InitializeInternalAsync(IIGPRuntimeContext context, CancellationToken cancellationToken)
        {
            if (coreRuntime == null || !ReferenceEquals(coreRuntime, context))
            {
                ErrorOccurred?.Invoke(new IGPMultiplayerErrorEvent(
                    "MULTIPLAYER_RUNTIME_CONTEXT_INVALID",
                    "IGPMultiplayerRuntime must be on the same GameObject as its IGPRuntimeManager.",
                    "runtime",
                    null,
                    true));
                return false;
            }

            initializeRequested = true;
            runtimeContext = context;
            cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(context.LifetimeToken, cancellationToken);
            InitializeSDKClients();
            Network = new IGPNetwork(this);
            Network.DataReceivedInternal += HandleBackendDataReceived;
            Network.PeerActivity += HandleBackendPeerActivity;
            ApplyLogSettings();
            playerId = string.IsNullOrEmpty(playerId) ? GeneratePlayerId() : playerId;
            playerName = string.IsNullOrEmpty(playerName) ? GeneratePlayerName() : playerName;
            desktopNotificationSource = (IIGPDesktopNotificationSource)context;
            desktopNotificationSource.NotificationReceived += HandleDesktopNotification;
            sdkInitialized = true;

            if (IGPLaunchArgsParser.TryParse(Environment.GetCommandLineArgs(), out var options) && options != null)
            {
                pendingLaunchOptions = options;
            }

            if (HasHostedLaunchContext(pendingLaunchOptions) && !await TryBootstrapFromLaunchTicketAsync())
            {
                return false;
            }

            if (!IsHostedSessionAttached && context.Desktop.IsAttached && context.Desktop.SupportsCapability(IGPDesktopHostedCommands.CapabilityFieldNumber))
            {
                _ = TryRequestHostedBootstrapFromDesktopSessionAsync();
            }

            UpdateRealtimeConnectionState(IGPRealtimeConnectionState.WaitingForRoom);
            return true;
        }

        public Task ShutdownAsync(CancellationToken cancellationToken = default)
        {
            shutdownTask ??= ShutdownInternalAsync(cancellationToken);
            return shutdownTask;
        }

        private async Task ShutdownInternalAsync(CancellationToken cancellationToken)
        {
            sdkInitialized = false;
            if (desktopNotificationSource != null)
            {
                desktopNotificationSource.NotificationReceived -= HandleDesktopNotification;
                desktopNotificationSource = null;
            }
            terminalDataPlaneFailure = true;
            cancellationTokenSource?.Cancel();
            await DisconnectAsync();
            if (Network != null)
            {
                Network.DataReceivedInternal -= HandleBackendDataReceived;
                Network.PeerActivity -= HandleBackendPeerActivity;
            }
            Network?.Dispose();
            KcpClient?.Dispose();
            UnreliableUdpClient?.Dispose();
            cancellationTokenSource?.Dispose();
            cancellationTokenSource = null;
        }

        private void Update()
        {
            if (!sdkInitialized || isDestroyed) return;
            ApplyLogSettingsIfNeeded();
            Network?.Update();
            dataPlaneTransport?.Tick();
            UnreliableUdpClient?.Tick();
            RecoverHostedDataPlaneStatusIfAlive();
            DriveAutomaticDataPlane();
        }

        private void DriveAutomaticDataPlane()
        {
            if (terminalDataPlaneFailure)
            {
                if (realtimeConnectionState != IGPRealtimeConnectionState.Idle)
                    UpdateRealtimeConnectionState(IGPRealtimeConnectionState.Idle);
                return;
            }

            if (!IsHostedSessionAttached || string.IsNullOrWhiteSpace(currentRoomId) || string.IsNullOrWhiteSpace(playerId))
            {
                if (realtimeConnectionState != IGPRealtimeConnectionState.WaitingForRoom)
                    UpdateRealtimeConnectionState(IGPRealtimeConnectionState.WaitingForRoom);
                return;
            }

            var nowUnixMs = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
            if (currentHostedDataPlaneExpiresAtUnixMs > 0 && nowUnixMs >= currentHostedDataPlaneExpiresAtUnixMs)
            {
                MarkHostedDataPlaneInterrupted("Hosted data-plane descriptor expired");
                return;
            }

            if (IsRealtimeReady)
            {
                reconnectAttempt = 0;
                return;
            }

            if (automaticDataPlaneTask != null && !automaticDataPlaneTask.IsCompleted) return;
            if (DateTime.UtcNow < nextDataPlaneAttemptUtc) return;
            automaticDataPlaneTask = RunAutomaticDataPlaneAttemptAsync(realtimeRoomGeneration);
        }

        private async Task RunAutomaticDataPlaneAttemptAsync(int roomGeneration)
        {
            try
            {
                await ConnectDataPlaneAsync();
                reconnectAttempt = 0;
                nextDataPlaneAttemptUtc = DateTime.MinValue;
            }
            catch (OperationCanceledException) when (RuntimeCancellationToken.IsCancellationRequested)
            {
            }
            catch (Exception ex)
            {
                if (roomGeneration != realtimeRoomGeneration || terminalDataPlaneFailure) return;
                var delay = ReconnectDelays[Math.Min(reconnectAttempt, ReconnectDelays.Length - 1)];
                reconnectAttempt++;
                nextDataPlaneAttemptUtc = DateTime.UtcNow + delay;
                LogSdkWarning("data-plane-reconnect", $"event=scheduled delaySeconds={delay.TotalSeconds:F0} error={FormatNetworkLogValue(ex.Message)}");
            }
        }

        private void HandleDesktopNotification(IGPDesktopSessionCommandResult result)
        {
            if (string.Equals(result.RequestId, IGPDesktopHostedCommands.BootstrapAvailableRequestId, StringComparison.Ordinal))
            {
                _ = TryRequestHostedBootstrapFromDesktopSessionAsync();
            }
        }

        private void HandleBackendDataReceived(IGPDataReceived packet) => backendDataReceived?.Invoke(packet);

        private void HandleBackendPeerActivity(IGPPeerActivity activity) => backendPeerActivity?.Invoke(activity);

        private void InitializeSDKClients()
        {
            KcpClient?.Dispose();
            UnreliableUdpClient?.Dispose();
            KcpClient = new IGPKcpClient();
            KcpClient.MessageReceived += HandleTransportMessage;
            KcpClient.ConnectionChanged += HandleTransportConnectionChanged;
            KcpClient.Faulted += HandleTransportFault;
            UnreliableUdpClient = new IGPUnreliableUdpClient();
            UnreliableUdpClient.MessageReceived += HandleTransportMessage;
            UnreliableUdpClient.ConnectionChanged += HandleTransportConnectionChanged;
            UnreliableUdpClient.Faulted += HandleTransportFault;
            ApplyKcpRuntimeSettings();
        }

        private void HandleTransportMessage(IGPTransportMessageReceived received)
        {
            var message = received.Message;
            if (TryHandleBusinessErrorMessage(message)) return;
            try
            {
                message.content = IGPRealtimeContentConverter.ConvertIncoming(message);
            }
            catch (Exception ex)
            {
                ErrorOccurred?.Invoke(new IGPMultiplayerErrorEvent(
                    "REALTIME_CONTENT_PARSE_FAILED",
                    $"Failed to parse realtime {message.type} content: {ex.Message}",
                    "protocol",
                    received.Transport,
                    false,
                    ex));
                return;
            }
            if (string.Equals(message.type, "scene_gate_set", StringComparison.OrdinalIgnoreCase)) { HandleSceneGateSet(message); return; }
            if (string.Equals(message.type, "scene_gate_get", StringComparison.OrdinalIgnoreCase)) { HandleSceneGateGet(message); return; }
            if (message.type == "p2p_data") { Network?.HandleIncomingP2P(message.content, received.Transport); return; }
            if (message.type == "team_change" || message.type == "team_initialize" || message.type == "team_status")
            {
                HandleTeamMessage(message);
                return;
            }
            if (IsInternalControlMessage(message.type)) return;
            MessageReceived?.Invoke(new IGPMessageReceivedEvent(
                message.type,
                message.roomId,
                message.playerId,
                message.targetPlayerId,
                message.content,
                message.timestamp));
        }

        private void HandleTransportConnectionChanged(IGPTransportConnectionChanged change)
        {
            if (change.Transport == IGPRealtimeTransport.Kcp)
            {
                HandleKcpConnectionStateChanged(change.IsConnected);
            }
        }

        private void HandleTransportFault(IGPTransportFault fault)
        {
            if (fault.Transport == IGPRealtimeTransport.Kcp && fault.IsTerminal)
            {
                _ = MarkHostedDataPlaneInterrupted(
                    fault.Message,
                    fault.Code,
                    fault.Transport,
                    fault.Details);
                return;
            }

            PublishMultiplayerError(
                fault.Code,
                fault.Message,
                "transport",
                fault.Transport,
                fault.IsTerminal,
                fault.Details);
        }

        private static bool IsInternalControlMessage(string? messageType)
        {
            return string.Equals(messageType, "ping", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "pong", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "kcp_token_request", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "kcp_token", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "room_command", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "room_command_result", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "room_join", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "room_leave", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "room_host_transfer", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "room_updated", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "room_summary_updated", StringComparison.OrdinalIgnoreCase) ||
                   string.Equals(messageType, "room_summary_deleted", StringComparison.OrdinalIgnoreCase);
        }

        private void PublishMultiplayerError(
            string code,
            string message,
            string source,
            IGPRealtimeTransport? transport = null,
            bool isTerminal = false,
            object? details = null)
        {
            ErrorOccurred?.Invoke(new IGPMultiplayerErrorEvent(
                code,
                message,
                source,
                transport,
                isTerminal,
                details));
        }

        private void HandleTeamMessage(Message message)
        {
            var previousRoom = CloneRoom(roomData);
            var token = message.content as JToken ?? JToken.FromObject(message.content ?? new object());

            if (string.Equals(message.type, "team_change", StringComparison.OrdinalIgnoreCase))
            {
                var changedPlayerId = token.Value<string>("playerId") ?? message.playerId;
                var teamId = token.Value<string>("teamId") ?? string.Empty;
                if (roomData.players != null && roomData.players.TryGetValue(changedPlayerId, out var player))
                {
                    player.teamId = teamId;
                    if (string.Equals(currentPlayerData.id, changedPlayerId, StringComparison.Ordinal))
                    {
                        currentPlayerData.teamId = teamId;
                    }
                }
            }
            else if (string.Equals(message.type, "team_initialize", StringComparison.OrdinalIgnoreCase))
            {
                roomData.teams = token["teams"]?.ToObject<Team[]>() ?? Array.Empty<Team>();
            }
            else
            {
                var statuses = token["teams"]?.ToObject<TeamStatusInfo[]>() ?? Array.Empty<TeamStatusInfo>();
                foreach (var status in statuses)
                {
                    var team = Array.Find(roomData.teams ?? Array.Empty<Team>(), item =>
                        string.Equals(item.id, status.teamId, StringComparison.Ordinal));
                    if (team != null)
                    {
                        team.playerCount = status.currentPlayers;
                        team.maxPlayers = status.maxPlayers;
                    }
                }
            }

            PublishRoomChangedIfNeeded(previousRoom, IGPRoomChangeKind.Updated, "realtime-team");
        }

        private void UpdateRealtimeConnectionState(IGPRealtimeConnectionState state, string reason = "")
        {
            if (realtimeConnectionState == state) return;
            var previousState = realtimeConnectionState;
            realtimeConnectionState = state;
            backendConnectionChanged?.Invoke(state);
            ConnectionChanged?.Invoke(new IGPMultiplayerConnectionChangedEvent(
                previousState,
                state,
                state == IGPRealtimeConnectionState.Ready,
                reason));
        }

        private void PublishRoomChangedIfNeeded(
            Room? previousRoom,
            IGPRoomChangeKind kind,
            string reason,
            IGPRoomChangeFlags? forcedChanges = null)
        {
            var currentRoom = kind == IGPRoomChangeKind.Left ? null : CloneRoom(roomData);
            var changes = forcedChanges ?? ResolveRoomChanges(previousRoom, currentRoom);
            if (kind == IGPRoomChangeKind.Updated && changes == IGPRoomChangeFlags.None)
            {
                return;
            }

            RoomChanged?.Invoke(new IGPRoomChangedEvent(
                kind,
                CloneRoom(previousRoom),
                currentRoom,
                kind == IGPRoomChangeKind.Left ? null : ClonePlayer(currentPlayerData),
                changes,
                reason));
        }

        private static IGPRoomChangeFlags ResolveRoomChanges(Room? previous, Room? current)
        {
            if (previous == null || current == null)
            {
                return IGPRoomChangeFlags.None;
            }

            var changes = IGPRoomChangeFlags.None;
            if (!JsonEquivalent(previous.players, current.players)) changes |= IGPRoomChangeFlags.Players;
            if (!string.Equals(previous.hostId, current.hostId, StringComparison.Ordinal)) changes |= IGPRoomChangeFlags.Host;
            if (!string.Equals(previous.status, current.status, StringComparison.Ordinal)) changes |= IGPRoomChangeFlags.Status;
            if (!string.Equals(previous.mapPublicId, current.mapPublicId, StringComparison.Ordinal) ||
                previous.mapVersionId != current.mapVersionId) changes |= IGPRoomChangeFlags.Map;
            if (!string.Equals(previous.sceneGate, current.sceneGate, StringComparison.Ordinal)) changes |= IGPRoomChangeFlags.SceneGate;
            if (!JsonEquivalent(previous.teams, current.teams)) changes |= IGPRoomChangeFlags.Teams;
            if (!string.Equals(previous.name, current.name, StringComparison.Ordinal) ||
                !string.Equals(previous.code, current.code, StringComparison.Ordinal) ||
                previous.maxPlayers != current.maxPlayers ||
                !JsonEquivalent(previous.globalState, current.globalState)) changes |= IGPRoomChangeFlags.Metadata;
            return changes;
        }

        private static bool JsonEquivalent(object? left, object? right) =>
            JToken.DeepEquals(
                left == null ? JValue.CreateNull() : JToken.FromObject(left),
                right == null ? JValue.CreateNull() : JToken.FromObject(right));

        private static Room? CloneRoom(Room? room) =>
            room == null ? null : JsonConvert.DeserializeObject<Room>(JsonConvert.SerializeObject(room));

        private static Player? ClonePlayer(Player? player) =>
            player == null ? null : JsonConvert.DeserializeObject<Player>(JsonConvert.SerializeObject(player));

        public async Task DisconnectAsync()
        {
            hostedDisconnectRequested = true;
            terminalDataPlaneFailure = true;
            var previousRoom = CloneRoom(roomData);
            InvalidateRealtimeDataPlane("idle");
            await DetachHostedSessionAsync();
            Network?.ResetSessionState();
            currentRoomId = string.Empty;
            currentRoomCode = string.Empty;
            sessionToken = string.Empty;
            roomData = new Room();
            currentPlayerData = new Player();
            hostedConnectionStatus = "disconnected";
            if (!isDestroyed && previousRoom != null && !string.IsNullOrEmpty(previousRoom.id))
            {
                RoomChanged?.Invoke(new IGPRoomChangedEvent(
                    IGPRoomChangeKind.Left,
                    previousRoom,
                    null,
                    null,
                    IGPRoomChangeFlags.None,
                    "disconnect"));
            }
            UpdateRealtimeConnectionState(IGPRealtimeConnectionState.Idle, "disconnect");
        }

        public void Disconnect() => _ = DisconnectAsync();

        public IGPNetworkResult CloseSession(IGPPlayerID localPeer, IGPPlayerID remotePeer) => Network.CloseSession(localPeer, remotePeer);
        public IGPNetworkResult Send(IGPPlayerID localPeer, IGPPlayerID remotePeer, byte[] data, uint messageType, bool reliable) =>
            reliable
                ? Network.SendReliableData(localPeer, remotePeer, data, (uint)data.Length, messageType)
                : Network.SendUnreliableData(localPeer, remotePeer, data, (uint)data.Length, messageType);

        private void EnsureSDKInitialized()
        {
            if (!initializeRequested || !sdkInitialized) throw new IGPSDKException("IGP Multiplayer is not initialized. Initialize IGPRuntimeManager first.");
        }

        private static bool HasHostedLaunchContext(IGPLaunchOptions? options) =>
            options != null &&
            !string.IsNullOrWhiteSpace(options.launchTicket) &&
            !string.IsNullOrWhiteSpace(options.bootstrapEndpoint) &&
            !string.IsNullOrWhiteSpace(options.bootstrapSecret);

        private int? ResolveDesktopAppId(int? appIdOverride = null)
        {
            if (appIdOverride > 0) return appIdOverride;
            if (runtimeContext?.Desktop.AttachedAppId > 0) return runtimeContext.Desktop.AttachedAppId;
            return Config?.appId > 0 ? Config.appId : null;
        }

        private static string ResolveSdkVersion() => IGPSdkVersion.PackageVersion;
        private string DesktopPipeEndpoint => "core-desktop-gateway";
        private string GeneratePlayerId() => $"player_{Guid.NewGuid().ToString("N").Substring(0, 8)}";
        private static string GeneratePlayerName() => "Player_" + UnityEngine.Random.Range(1000, 9999);

        private void OnDestroy()
        {
            isDestroyed = true;
            _ = ShutdownAsync();
        }

        private void OnDisable()
        {
            if (sdkInitialized)
            {
                _ = ShutdownAsync();
            }
        }
    }

    internal sealed class StaleHostedDataPlaneAttemptException : Exception { }
    internal static class MissingStateDefaultSentinel
    {
        internal static readonly object Value = new object();
    }
}
