#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.Multiplayer.Protocol;
using IGP.UnitySDK;
using IGP.UnitySDK.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace IGP.Multiplayer
{
    [RequireComponent(typeof(IGPRuntimeManager))]
    public partial class IGPMultiplayerRuntime : MonoBehaviour,
        IIGPRuntimeExtension,
        IIGPMultiplayerTransportBackend
    {
        private static readonly TimeSpan HostedDataPlaneReadyTimeout = TimeSpan.FromSeconds(5);
        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("Reliable Transport")]
        [SerializeField] private IGPReliableTransportPreference reliableTransportPreference = IGPReliableTransportPreference.KcpOnly;

        [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<IGPMultiplayerConnectionChangedEvent>? ConnectionInterrupted;
        public event Action<IGPMultiplayerConnectionChangedEvent>? ConnectionRecovered;
        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 IIGPMultiplayerAccessProvider? multiplayerAccessProvider;
        private CancellationTokenSource? cancellationTokenSource;
        private bool isDestroyed;
        private bool initializeRequested;
        private bool extensionInitialized;
        private bool sdkInitialized;
        private bool runtimeCreationInProgress;
        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 long currentHostedDataPlaneExpiresAtUnixSeconds;
        private string currentHostedDataPlaneRoomId = string.Empty;
        private string currentHostedDataPlanePlayerId = string.Empty;
        private string currentHostedDataPlaneStatus = "idle";
        private string currentHostedDataPlaneError = string.Empty;
        private IIGPNetworkTransport? reliableTransport;
        private IIGPNetworkTransport? unreliableTransport;
        private int realtimeRoomGeneration;
        private int hostedDataPlaneAttemptId;
        private bool hostedDisconnectRequested;
        private bool hostedBootstrapRequestInFlight;
        private bool suppressReliableDisconnectHandling;
        private bool tcpFallbackEligible;
        private int lastPublishedTransportFaultAttemptId;
        private bool pendingConnectionRecoveryNotification;
        private bool terminalDataPlaneFailure;
        private IGPLogLevel appliedLogLevel = IGPLogLevel.Off;
        private readonly SemaphoreSlim runtimeCreationGate = new SemaphoreSlim(1, 1);

        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 IsRuntimeCreated => 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 IGPTcpClient? TcpClient { get; set; }
        private IGPUdpClient? UdpClient { get; set; }
        public IGPNetwork? Network { get; private set; }

        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 IGPReliableTransportPreference ReliableTransportPreference
        {
            get => reliableTransportPreference;
            set
            {
                if (value != IGPReliableTransportPreference.KcpOnly &&
                    value != IGPReliableTransportPreference.TcpOnly &&
                    value != IGPReliableTransportPreference.PreferTcp)
                {
                    throw new ArgumentOutOfRangeException(nameof(value));
                }
                reliableTransportPreference = value;
            }
        }
        public IGPRealtimeTransport? CurrentReliableTransport => reliableTransport?.Transport;
        public bool IsReliableConnected => reliableTransport?.IsConnected == true;
        public bool IsReliableAlive => reliableTransport?.IsAlive == true;
        public bool IsKcpConnected => reliableTransport?.Transport == IGPRealtimeTransport.Kcp && reliableTransport.IsConnected;
        internal bool UsesDataPlaneEnvelopeV1 => reliableTransport?.NegotiatedEnvelopeVersion == IGPDataPlaneEnvelopeCodec.Version;
        public bool IsUnreliableUdpConnected => unreliableTransport?.IsConnected ?? false;
        public int UnreliableUdpPayloadMaxBytes => UdpClient?.PayloadMaxBytes ?? IGPUdpClient.DefaultPayloadMaxBytes;
        public IGPDataPlaneMode CurrentDataPlaneMode => currentDataPlaneMode;
        public bool IsRealtimeReady => realtimeConnectionState == IGPRealtimeConnectionState.Ready && IsReliableConnected;
        public string CurrentHostedDataPlaneHost => currentHostedDataPlaneHost;
        public uint CurrentHostedDataPlanePort => currentHostedDataPlanePort;
        public string CurrentHostedDataPlaneStatus => currentHostedDataPlaneStatus;
        public string CurrentHostedDataPlaneError => currentHostedDataPlaneError;
        public IGPRealtimeConnectionState RealtimeConnectionState => realtimeConnectionState;
        public IGPRTTStats? KcpRTTStats => CurrentReliableTransport == IGPRealtimeTransport.Kcp ? KcpClient?.RTTStats : null;
        public IGPRTTStats? AppRTTStats => reliableTransport?.AppRTTStats;
        public IGPKcpTransportStats KcpTransportStats => CurrentReliableTransport == IGPRealtimeTransport.Kcp
            ? KcpClient?.TransportStats ?? IGPKcpTransportStats.Unavailable
            : IGPKcpTransportStats.Unavailable;
        public IGPKcpHealthState KcpHealthState => CurrentReliableTransport == IGPRealtimeTransport.Kcp
            ? KcpClient?.HealthState ?? IGPKcpHealthState.Disconnected
            : IGPKcpHealthState.Disconnected;
        public IGPKcpFaultReason KcpLastFaultReason => CurrentReliableTransport == IGPRealtimeTransport.Kcp
            ? KcpClient?.LastFaultReason ?? IGPKcpFaultReason.None
            : IGPKcpFaultReason.None;
        public bool IsKcpAlive => reliableTransport?.Transport == IGPRealtimeTransport.Kcp && reliableTransport.IsAlive;

        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>();
            multiplayerAccessProvider = coreRuntime as IIGPMultiplayerAccessProvider;
        }

        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))
            {
                IGPLog.Error(
                    "multiplayer-runtime",
                    "initialize",
                    "event=failed reason=runtime-context-invalid");
                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);
            ApplyLogSettings();
            LogSdkInfo(
                "multiplayer-runtime",
                "initialize",
                $"event=start descriptorSource=core-host reliablePreference={reliableTransportPreference}");
            multiplayerAccessProvider = context as IIGPMultiplayerAccessProvider;
            extensionInitialized = true;

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

            if (multiplayerAccessProvider != null)
            {
                multiplayerAccessProvider.RoomAccessAvailable += HandleRoomAccessAvailable;
            }

            UpdateRealtimeConnectionState(IGPRealtimeConnectionState.WaitingForRoom);
            await TryCreateRuntimeAsync(cancellationToken);
            LogSdkInfo(
                "multiplayer-runtime",
                "initialize",
                $"event=complete descriptorSource=core-host runtimeCreated={sdkInitialized} state={realtimeConnectionState}");
            return true;
        }

        public Task ShutdownAsync(CancellationToken cancellationToken = default)
        {
            return RequestShutdown("explicit", cancellationToken);
        }

        private Task RequestShutdown(string reason, CancellationToken cancellationToken = default)
        {
            if (shutdownTask != null)
            {
                LogSdkDebug(
                    "multiplayer-runtime",
                    "shutdown",
                    $"event=skipped reason=already-requested caller={FormatNetworkLogValue(reason)}");
                return shutdownTask;
            }

            LogSdkInfo(
                "multiplayer-runtime",
                "shutdown",
                $"event=requested reason={FormatNetworkLogValue(reason)} state={realtimeConnectionState} " +
                $"reliableTransport={CurrentReliableTransport?.ToString() ?? "-"} reliableConnected={IsReliableConnected}");
            shutdownTask = ShutdownInternalAsync(reason, cancellationToken);
            return shutdownTask;
        }

        private async Task ShutdownInternalAsync(string reason, CancellationToken cancellationToken)
        {
            extensionInitialized = false;
            sdkInitialized = false;
            if (multiplayerAccessProvider != null)
            {
                multiplayerAccessProvider.RoomAccessAvailable -= HandleRoomAccessAvailable;
            }
            terminalDataPlaneFailure = true;
            cancellationTokenSource?.Cancel();
            await DisconnectInternalAsync($"shutdown:{reason}");
            if (Network != null)
            {
                Network.DataReceivedInternal -= HandleBackendDataReceived;
                Network.PeerActivity -= HandleBackendPeerActivity;
            }
            DisposeRuntimeResources($"shutdown:{reason}");
            cancellationTokenSource?.Dispose();
            cancellationTokenSource = null;
            LogSdkInfo(
                "multiplayer-runtime",
                "shutdown",
                $"event=complete reason={FormatNetworkLogValue(reason)}");
        }

        private void Update()
        {
            if (isDestroyed || !extensionInitialized) return;
            Network?.Update();
            ApplyLogSettingsIfNeeded();
            reliableTransport?.Tick();
            unreliableTransport?.Tick();
        }

        private void HandleRoomAccessAvailable()
        {
            if (terminalDataPlaneFailure ||
                sdkInitialized ||
                pendingConnectionRecoveryNotification ||
                realtimeConnectionState == IGPRealtimeConnectionState.Failed)
            {
                return;
            }

            _ = TryCreateRuntimeAsync();
        }

        private void EnsureNetworkRuntime()
        {
            if (Network != null) return;
            Network = new IGPNetwork(this);
            Network.DataReceivedInternal += HandleBackendDataReceived;
            Network.PeerActivity += HandleBackendPeerActivity;
        }

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

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

        private void InitializeSDKClients()
        {
            LogSdkDebug(
                "multiplayer-runtime",
                "resources",
                $"event=clients-create replacingKcp={KcpClient != null} replacingTcp={TcpClient != null} replacingUdp={UdpClient != null}");
            KcpClient?.Dispose();
            TcpClient?.Dispose();
            UdpClient?.Dispose();
            KcpClient = new IGPKcpClient();
            KcpClient.DowngradeErrorsToWarnings = runtimeCreationInProgress;
            TcpClient = new IGPTcpClient();
            UdpClient = new IGPUdpClient();
            ApplyKcpRuntimeSettings();
        }

        private void EnsureRuntimeResources()
        {
            if (sdkInitialized) return;
            InitializeSDKClients();
            EnsureNetworkRuntime();
            ApplyLogSettings();
            terminalDataPlaneFailure = false;
            sdkInitialized = true;
        }

        private void DisposeRuntimeResources(string reason)
        {
            LogSdkDebug(
                "multiplayer-runtime",
                "resources",
                $"event=dispose-start reason={FormatNetworkLogValue(reason)} state={realtimeConnectionState} " +
                $"mode={currentDataPlaneMode} reliableTransport={CurrentReliableTransport?.ToString() ?? "-"} " +
                $"reliableConnected={IsReliableConnected} unreliableConnected={IsUnreliableUdpConnected}");
            DetachNetworkTransportEvents(reliableTransport);
            reliableTransport?.Disconnect();
            reliableTransport = null;
            DetachNetworkTransportEvents(unreliableTransport);
            unreliableTransport?.Disconnect();
            unreliableTransport = null;
            pendingUnreliableDescriptor = null;
            if (Network != null)
            {
                Network.DataReceivedInternal -= HandleBackendDataReceived;
                Network.PeerActivity -= HandleBackendPeerActivity;
                Network.Dispose();
                Network = null;
            }
            KcpClient?.Dispose();
            TcpClient?.Dispose();
            UdpClient?.Dispose();
            KcpClient = null;
            TcpClient = null;
            UdpClient = null;
            currentDataPlaneMode = IGPDataPlaneMode.None;
            currentHostedDataPlaneHost = string.Empty;
            currentHostedDataPlanePort = 0;
            currentHostedDataPlaneExpiresAtUnixSeconds = 0;
            currentHostedDataPlaneRoomId = string.Empty;
            currentHostedDataPlanePlayerId = string.Empty;
            currentHostedDataPlaneStatus = "idle";
            currentHostedDataPlaneError = string.Empty;
            sdkInitialized = false;
            LogSdkDebug(
                "multiplayer-runtime",
                "resources",
                $"event=dispose-complete reason={FormatNetworkLogValue(reason)}");
        }

        private void AttachNetworkTransportEvents(IIGPNetworkTransport transport)
        {
            transport.MessageReceived += HandleTransportMessage;
            transport.ConnectionChanged += HandleTransportConnectionChanged;
            transport.Faulted += HandleTransportFault;
        }

        private void DetachNetworkTransportEvents(IIGPNetworkTransport? transport)
        {
            if (transport == null) return;
            transport.MessageReceived -= HandleTransportMessage;
            transport.ConnectionChanged -= HandleTransportConnectionChanged;
            transport.Faulted -= HandleTransportFault;
        }

        private void HandleTransportMessage(IGPTransportMessageReceived received)
        {
            var message = received.Message;
            if (TryHandleBusinessErrorMessage(message)) return;
            try
            {
                message.content = IGPRealtimeContentConverter.ConvertIncoming(message);
            }
            catch (Exception ex)
            {
                LogSdkWarning(
                    "protocol",
                    "decode",
                    $"event=content-parse-failed transport={received.Transport} messageType={FormatNetworkLogValue(message.type)} " +
                    $"error={FormatNetworkLogValue(ex.Message)}");
                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)
        {
            LogSdkInfo(
                "transport",
                "connection",
                $"event=changed transport={change.Transport} connected={change.IsConnected} " +
                $"reason={FormatNetworkLogValue(change.Reason)} suppressed={suppressReliableDisconnectHandling} " +
                $"activeReliable={CurrentReliableTransport?.ToString() ?? "-"}");
            if (reliableTransport?.Transport == change.Transport)
            {
                HandleReliableConnectionStateChanged(change.IsConnected, change.Transport);
            }
        }

        private void HandleTransportFault(IGPTransportFault fault)
        {
            if (fault.Transport == IGPRealtimeTransport.Tcp && TcpClient != null)
            {
                string message = $"event=terminal-fault code={FormatNetworkLogValue(fault.Code)} " +
                                 $"message={FormatNetworkLogValue(fault.Message)} {TcpClient.DiagnosticsSummary}";
                if (runtimeCreationInProgress)
                    LogSdkWarning("tcp", "fault", message);
                else
                    LogSdkError("tcp", "fault", message);
            }

            if (reliableTransport?.Transport == fault.Transport && fault.IsTerminal)
            {
                if (realtimeConnectionState == IGPRealtimeConnectionState.Connecting ||
                    realtimeConnectionState == IGPRealtimeConnectionState.RequestingDescriptor)
                {
                    bool willFallback = fault.Transport == IGPRealtimeTransport.Tcp && tcpFallbackEligible;
                    currentHostedDataPlaneStatus = "error";
                    currentHostedDataPlaneError = fault.Message;
                    if (!runtimeCreationInProgress)
                    {
                        lastPublishedTransportFaultAttemptId = hostedDataPlaneAttemptId;
                        PublishMultiplayerError(
                            fault.Code,
                            fault.Message,
                            "transport",
                            fault.Transport,
                            !willFallback,
                            fault.Details);
                    }
                    return;
                }
                _ = MarkHostedDataPlaneInterrupted(
                    fault.Message,
                    fault.Code,
                    fault.Transport,
                    fault.Details);
                return;
            }

            if (runtimeCreationInProgress)
            {
                LogSdkWarning(
                    "transport",
                    "runtime-create",
                    $"event=fault code={FormatNetworkLogValue(fault.Code)} message={FormatNetworkLogValue(fault.Message)}");
            }
            else
            {
                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;
            LogSdkInfo(
                "multiplayer-runtime",
                "connection-state",
                $"event=changed previous={previousState} current={state} reason={FormatNetworkLogValue(reason)} " +
                $"dataPlaneStatus={FormatNetworkLogValue(currentHostedDataPlaneStatus)} " +
                $"reliableTransport={CurrentReliableTransport?.ToString() ?? "-"} reliableConnected={IsReliableConnected}");
            backendConnectionChanged?.Invoke(state);
            ConnectionChanged?.Invoke(new IGPMultiplayerConnectionChangedEvent(
                previousState,
                state,
                state == IGPRealtimeConnectionState.Ready,
                reason));
        }

        private void PublishConnectionRecoveredIfPending(string reason)
        {
            if (!pendingConnectionRecoveryNotification ||
                isDestroyed ||
                !IsRealtimeReady)
            {
                return;
            }

            pendingConnectionRecoveryNotification = false;
            LogSdkInfo(
                "multiplayer-runtime",
                "connection-recovery",
                $"event=recovered reason={FormatNetworkLogValue(reason)} transport={CurrentReliableTransport?.ToString() ?? "-"}");
            ConnectionRecovered?.Invoke(new IGPMultiplayerConnectionChangedEvent(
                IGPRealtimeConnectionState.Failed,
                IGPRealtimeConnectionState.Ready,
                true,
                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 Task DisconnectAsync()
        {
            return DisconnectInternalAsync("explicit");
        }

        private async Task DisconnectInternalAsync(string reason)
        {
            hostedDisconnectRequested = true;
            terminalDataPlaneFailure = true;
            pendingConnectionRecoveryNotification = false;
            var previousRoom = CloneRoom(roomData);
            LogSdkInfo(
                "multiplayer-runtime",
                "disconnect",
                $"event=start reason={FormatNetworkLogValue(reason)} roomId={FormatNetworkLogValue(currentRoomId)} " +
                $"state={realtimeConnectionState} reliableTransport={CurrentReliableTransport?.ToString() ?? "-"} " +
                $"reliableConnected={IsReliableConnected}");
            InvalidateRealtimeDataPlane($"idle:{reason}");
            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");
            LogSdkInfo(
                "multiplayer-runtime",
                "disconnect",
                $"event=complete reason={FormatNetworkLogValue(reason)}");
        }

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

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

        private void EnsureExtensionInitialized()
        {
            if (!initializeRequested || !extensionInitialized)
                throw new IGPSDKException("IGP Multiplayer is unavailable. 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? ResolveHostAppId() => Config?.appId > 0 ? Config.appId : null;

        private static string ResolveSdkVersion() => IGPSdkVersion.PackageVersion;
        private void OnDestroy()
        {
            isDestroyed = true;
            _ = RequestShutdown("on-destroy");
        }
    }

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