#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
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;
using Debug = UnityEngine.Debug;

namespace IGP.UnitySDK
{
    /// <summary>
    /// 游戏状态枚举（用于自适应心跳间隔）
    /// </summary>
    public enum GameState
    {
        /// <summary>空闲状态（大厅、菜单）</summary>
        Idle,
        /// <summary>正常游戏中</summary>
        InGame,
        /// <summary>激烈战斗中（需要精确RTT）</summary>
        InBattle
    }

    /// <summary>
    /// 当前数据面模式。
    /// </summary>
    public enum IGPDataPlaneMode
    {
        None = 0,
        DirectKcp = 1,
    }
    
    /// <summary>
    /// Unity事件定义
    /// </summary>
    [Serializable]
    public class IGPConnectionEvent : UnityEvent<bool> { }
    
    [Serializable]
    public class IGPMessageEvent : UnityEvent<string, object?> { }
    
    [Serializable]
    public class IGPErrorEvent : UnityEvent<string> { }
    
    [Serializable]
    public class IGPRoomEvent : UnityEvent<Room> { }

    [Serializable]
    public class IGPMapChangedEvent : UnityEvent<IGPMapChangeData> { }

    [Serializable]
    public class IGPAuthorizationStateEvent : UnityEvent<IGPAuthorizationState> { }

    [Serializable]
    public class IGPAuthorizationFailedEvent : UnityEvent<string> { }

    [Serializable]
    public class IGPAntiAddictionStatusEvent : UnityEvent<IGPAntiAddictionStatus> { }

    /// <summary>
    /// 授权验证状态
    /// </summary>
    public enum IGPAuthorizationState
    {
        Pending = 0,
        AuthorizedOnline = 1,
        AuthorizedOffline = 2,
        Failed = 3,
        Skipped = 4,
    }

    [Serializable]
    public sealed class IGPAuthorizationFailureInfo
    {
        public string code = string.Empty;
        public string message = string.Empty;
    }

    /// <summary>
    /// 房主转移事件（from, to）
    /// </summary>
    [Serializable]
    public class IGPHostTransferEvent : UnityEvent<string, string> { }

    /// <summary>
    /// IGP Unity SDK 主组件
    /// </summary>
    public class IGPRuntimeManager : MonoBehaviour, IIGPRuntimeClient
    {
        private static readonly TimeSpan HostedRealtimeReadyTimeout = TimeSpan.FromSeconds(10);
        private static readonly TimeSpan HostedDataPlaneReadyTimeout = TimeSpan.FromSeconds(5);
        private static readonly TimeSpan HostedDataPlaneRetryDelay = TimeSpan.FromMilliseconds(250);
        private static readonly TimeSpan HostedRecoveryWindow = TimeSpan.FromSeconds(30);
        private static readonly TimeSpan HostedRecoveryInitialDelay = TimeSpan.FromMilliseconds(500);
        private static readonly TimeSpan HostedRecoveryMaxDelay = TimeSpan.FromSeconds(3);
        private const int HostedDataPlaneAttachAttempts = 3;
        private const int MaxAuthorizationPipeResponseBytes = 1024 * 1024;

        [Header("Global Configuration")]
        [SerializeField] private IGPConfig? config = null;

        [Header("Hosted Runtime Settings")]
        private string serverUrl = string.Empty;
        [NonSerialized] private float runtimeHeartbeatIntervalOverride;
        
        [Header("KCP Heartbeat Settings")]
        [Tooltip("KCP心跳间隔（秒）")]
        [SerializeField] private float kcpHeartbeatInterval = 1.0f;
        
        [Tooltip("是否启用自动KCP心跳")]
        [SerializeField] private bool enableKcpHeartbeat = true;
        
        [Header("RTT Monitoring")]
        [Tooltip("是否显示RTT统计（OnGUI）")]
        [SerializeField] private bool showRTTStats = false;

        [Header("Authorization Fallback")]
        [Tooltip("授权验证失败时是否显示 SDK 自带的保底提示（OnGUI）")]
        [SerializeField] private bool showAuthorizationFailureFallback = true;
        [Tooltip("默认保底提示标题。留空时使用 SDK 默认文案。")]
        [SerializeField] private string authorizationFailureFallbackTitle = "Unable to verify game access";
        [Tooltip("默认保底提示正文。留空时使用最近一次失败原因。")]
        [SerializeField] private string authorizationFailureFallbackMessage = string.Empty;
        [Tooltip("默认保底提示引导语。留空时使用 SDK 默认文案。")]
        [SerializeField] private string authorizationFailureFallbackHint =
            "Open IGP Desktop, make sure you are signed in, and then try again.";
        [Tooltip("默认保底提示确认按钮文案。留空时使用 SDK 默认文案。")]
        [SerializeField] private string authorizationFailureFallbackButtonText = "OK";
        
        [Header("Player Info")]
        [SerializeField] private string playerId = string.Empty;
        [SerializeField] private string playerName = string.Empty;
        public string playerColor = "#FF0000";
        public string playerAvatar = "default";
        public int playerAvatarIndex = 0;
        
        [Header("Room Info")]
        [SerializeField] private string currentRoomId = string.Empty;
        [SerializeField] private string currentRoomCode = string.Empty;
        [SerializeField] private string sessionToken = string.Empty;
        [SerializeField] private Room roomData = new Room();
        [SerializeField] private Player currentPlayerData = new Player();
        
        [Header("Events")]
        public IGPConnectionEvent onConnectionStateChanged = new IGPConnectionEvent();
        public IGPMessageEvent onMessageReceived = new IGPMessageEvent();
        public IGPErrorEvent onError = new IGPErrorEvent();
        public IGPAuthorizationStateEvent onAuthorizationStateChanged = new IGPAuthorizationStateEvent();
        public IGPAuthorizationFailedEvent onAuthorizationFailed = new IGPAuthorizationFailedEvent();
        public IGPAntiAddictionStatusEvent onAntiAddictionStateChanged = new IGPAntiAddictionStatusEvent();
        public IGPRoomEvent onRoomJoined = new IGPRoomEvent();
        public IGPRoomEvent onRoomUpdated = new IGPRoomEvent();
        public IGPRoomEvent onRoomLeft = new IGPRoomEvent();
        public IGPMapChangedEvent onMapChanged = new IGPMapChangedEvent();
        public UnityEvent onHeartbeatSent = new UnityEvent();

        // 队伍相关事件
        public IGPMessageEvent onTeamChanged = new IGPMessageEvent();
        public IGPMessageEvent onTeamInitialized = new IGPMessageEvent();
        public IGPMessageEvent onTeamStatusUpdated = new IGPMessageEvent();
        public IGPHostTransferEvent onHostTransferred = new IGPHostTransferEvent();

        internal event Action<IGPHostSessionRoomEvent>? HostedRoomEventReceived;
        
        // 内部状态
        private CancellationTokenSource? cancellationTokenSource;
        private Coroutine? heartbeatCoroutine;
        private bool isDestroyed = false;
        private bool initializeRequested = false;
        private bool sdkInitialized = false;
        private Task<bool>? sdkInitializationTask;
        private IGPLaunchOptions? pendingLaunchOptions;
        private IGPDesktopSessionClient? desktopSessionClient;
        private IGPHostSessionClient? hostSessionClient;
        private IGPDesktopUserContext? currentDesktopUserContext;
        private IGPDesktopCapabilitySet? currentDesktopCapabilities;
        private IGPDesktopUserProfile? currentDesktopUserProfile;
        private IGPAntiAddictionStatus? currentAntiAddictionStatus;
        private string desktopSessionLastErrorCode = string.Empty;
        private string desktopSessionLastErrorMessage = string.Empty;
        private string desktopSessionLastErrorCategory = string.Empty;
        private string desktopSessionLastChannelState = string.Empty;
        private string desktopSessionLastAttachState = string.Empty;
        private string desktopSessionLastAttachSource = string.Empty;
        // 自愈相关：下一次重试时间 + 防并发哨兵
        private DateTime? nextDesktopAttachRetryAtUtc;
        private bool desktopAttachRetryInFlight;
        private DateTime? nextAuthorizationRecoverAtUtc;
        private bool authorizationRecoverInFlight;
        private const double DesktopAttachRetryBackoffSeconds = 2.0;
        private const double AuthorizationRecoverBackoffSeconds = 2.0;
        private IGPAuthorizationState authorizationState = IGPAuthorizationState.Pending;
        private IGPAuthorizationFailureInfo? lastAuthorizationFailure;
        private bool isAuthorizationRequired;
        private string authorizationPipeName = string.Empty;
        private string authorizationBrokerToken = string.Empty;
        private string authorizationSessionToken = string.Empty;
        private string authorizationDeviceIdHash = string.Empty;
        private string authorizationOfflineLicensePublicKey = string.Empty;
        private string authorizationOfflineLicenseKeyId = string.Empty;
        private string authorizationOfflineLicenseAlgorithm = string.Empty;
        private DateTimeOffset? authorizationSessionExpiresAt;
        private int authorizationGameId;
        private bool authorizationFailureFallbackDismissed;
        private CancellationTokenSource? authorizationRefreshCts;
        private Task? authorizationRefreshTask;
        private string hostedConnectionStatus = "disconnected";
        private string currentHostedSessionEndpoint = string.Empty;
        private string currentHostedSessionSecret = string.Empty;
        private IGPDataPlaneMode currentDataPlaneMode = IGPDataPlaneMode.None;
        private string currentHostedDataPlaneHost = string.Empty;
        private uint currentHostedDataPlanePort;
        private string currentHostedDataPlaneStatus = "idle";
        private string currentHostedDataPlaneError = string.Empty;
        private IIGPDataPlaneTransport? dataPlaneTransport;
        private CancellationTokenSource? hostedRecoveryCts;
        private Task? hostedRecoveryTask;
        private bool hostedRecoveryRestoreDataPlane;
        private bool hostedRecoveryRequiresRealtime;
        private bool hostedDisconnectRequested;
        private bool hostedBootstrapRequestInFlight;
        private bool suppressKcpRecovery;
        
        // SDK客户端
        internal IGPHostedRealtimeClient? WebSocketClient { get; private set; }
        public IGPKcpClient? KcpClient { get; private set; }
        
        /// <summary>
        /// IGP Network 模块 (P2P 通信)
        /// </summary>
        public IGPNetwork? Network { get; private set; }

        private CancellationToken RuntimeCancellationToken => cancellationTokenSource?.Token ?? CancellationToken.None;
        
        #region Properties
        
        /// <summary>
        /// 服务器URL
        /// </summary>
        public string ServerUrl 
        { 
            get => serverUrl;
            set => serverUrl = value ?? string.Empty;
        }
        
        /// <summary>
        /// 心跳间隔
        /// </summary>
        public float HeartbeatInterval
        {
            get => runtimeHeartbeatIntervalOverride > 0 ? runtimeHeartbeatIntervalOverride : (config != null ? config.heartbeatInterval : 30f);
            set => runtimeHeartbeatIntervalOverride = value;
        }

        /// <summary>
        /// 当前运行实例是否已经完成初始化。
        /// </summary>
        public bool IsInitialized => sdkInitialized;

        /// <summary>
        /// 当前 SDK 连接的桌面端环境。
        /// </summary>
        public IGPSDKEnvironment SdkEnvironment => config != null ? config.sdkEnvironment : IGPSDKEnvironment.PROD;

        /// <summary>
        /// 是否自动连接
        /// </summary>
        public bool AutoConnect => (pendingLaunchOptions?.autoConnect ?? false) || (config != null && config.autoConnect);

        /// <summary>
        /// 当前配置资源。
        /// </summary>
        public IGPConfig? Config => config;

        /// <summary>
        /// 当前房间ID
        /// </summary>
        public string CurrentRoomId => currentRoomId;
        
        /// <summary>
        /// 当前房间代码
        /// </summary>
        public string CurrentRoomCode => currentRoomCode;

        /// <summary>
        /// 当前房间快照
        /// </summary>
        public Room CurrentRoomData => roomData;

        /// <summary>
        /// 当前玩家快照
        /// </summary>
        public Player CurrentPlayerData => currentPlayerData;

        /// <summary>
        /// 当前会话 Token
        /// </summary>
        public string SessionToken 
        {
            get => sessionToken;
            set => sessionToken = value;
        }
        
        /// <summary>
        /// WebSocket连接状态
        /// </summary>
        public bool IsWebSocketConnected => WebSocketClient?.IsConnected ?? false;

        /// <summary>
        /// desktop 能力通道是否已附着
        /// </summary>
        public bool IsDesktopSessionAttached => desktopSessionClient?.IsAttached ?? false;

        /// <summary>
        /// 当前 desktop 登录用户上下文
        /// </summary>
        public IGPDesktopUserContext? CurrentDesktopUserContext => currentDesktopUserContext;

        /// <summary>
        /// 当前 desktop 能力集合
        /// </summary>
        public IGPDesktopCapabilitySet? CurrentDesktopCapabilities => currentDesktopCapabilities;

        /// <summary>
        /// desktop session 通道状态（desktop 端上报的 channelState，例如 reachable / connecting / failed）。
        /// 对齐 GameMaker bridge，用于诊断与 UI 展示。
        /// </summary>
        public string DesktopChannelState => desktopSessionClient?.CurrentSession?.ChannelState
            ?? desktopSessionLastChannelState
            ?? string.Empty;

        /// <summary>
        /// desktop session attach 校验状态（desktop 端上报的 attachState，例如 attached / rejected / attaching）。
        /// 对齐 GameMaker bridge，用于诊断与 UI 展示。
        /// </summary>
        public string DesktopAttachState => desktopSessionClient?.CurrentSession?.AttachState
            ?? desktopSessionLastAttachState
            ?? string.Empty;

        /// <summary>
        /// desktop session attach 来源（desktop 端上报的 attachSource，例如 launcher / auto-discovery）。
        /// 对齐 GameMaker bridge。
        /// </summary>
        public string DesktopAttachSource => desktopSessionClient?.CurrentSession?.AttachSource
            ?? desktopSessionLastAttachSource
            ?? string.Empty;

        /// <summary>
        /// 最近一次 desktop session 失败的错误分类（validation/authorization/channel/runtime）。
        /// 对齐 GameMaker bridge，用于驱动上层的重试或回退策略。
        /// </summary>
        public string DesktopSessionLastErrorCategory => desktopSessionLastErrorCategory ?? string.Empty;

        /// <summary>
        /// 当前 desktop 登录用户资料
        /// </summary>
        public IGPDesktopUserProfile? CurrentDesktopUserProfile => currentDesktopUserProfile;

        /// <summary>
        /// 当前防沉迷状态快照。
        /// </summary>
        public IGPAntiAddictionStatus? CurrentAntiAddictionStatus => currentAntiAddictionStatus;

        /// <summary>
        /// 当前授权验证状态
        /// </summary>
        public IGPAuthorizationState AuthorizationState => authorizationState;

        /// <summary>
        /// 当前是否已通过授权验证
        /// </summary>
        public bool IsAuthorized =>
            authorizationState == IGPAuthorizationState.AuthorizedOnline ||
            authorizationState == IGPAuthorizationState.AuthorizedOffline;

        /// <summary>
        /// 当前是否要求授权验证
        /// </summary>
        public bool IsAuthorizationRequired => isAuthorizationRequired;

        /// <summary>
        /// 最近一次授权验证失败信息
        /// </summary>
        public IGPAuthorizationFailureInfo? LastAuthorizationFailure => lastAuthorizationFailure;

        /// <summary>
        /// 宿主控制通道是否已附着
        /// </summary>
        public bool IsHostedSessionAttached => hostSessionClient?.IsAttached ?? false;

        /// <summary>
        /// 当前宿主控制通道端点。
        /// </summary>
        public string CurrentHostedSessionEndpoint => currentHostedSessionEndpoint;
        
        /// <summary>
        /// KCP连接状态
        /// </summary>
        public bool IsKcpConnected => dataPlaneTransport?.IsConnected ?? (KcpClient?.IsConnected ?? false);

        /// <summary>
        /// 当前数据面模式。
        /// </summary>
        public IGPDataPlaneMode CurrentDataPlaneMode => currentDataPlaneMode;

        /// <summary>
        /// 当前是否已附着数据面。
        /// </summary>
        public bool IsHostedDataPlaneAttached => currentDataPlaneMode != IGPDataPlaneMode.None;

        /// <summary>
        /// 当前数据面主机地址。
        /// </summary>
        public string CurrentHostedDataPlaneHost => currentHostedDataPlaneHost;

        /// <summary>
        /// 当前数据面端口。
        /// </summary>
        public uint CurrentHostedDataPlanePort => currentHostedDataPlanePort;

        /// <summary>
        /// 当前数据面附着状态。
        /// </summary>
        public string CurrentHostedDataPlaneStatus => currentHostedDataPlaneStatus;

        /// <summary>
        /// 当前数据面附着失败原因。
        /// </summary>
        public string CurrentHostedDataPlaneError => currentHostedDataPlaneError;
        
        /// <summary>
        /// 玩家ID
        /// </summary>
        public string PlayerId 
        { 
            get => string.IsNullOrEmpty(playerId) ? GeneratePlayerId() : playerId; 
            set => playerId = value; 
        }
        
        /// <summary>
        /// 玩家名称
        /// </summary>
        public string PlayerName 
        { 
            get => string.IsNullOrEmpty(playerName) ? "Player_" + UnityEngine.Random.Range(1000, 9999) : playerName; 
            set => playerName = value; 
        }
        
        /// <summary>
        /// 获取KCP RTT统计
        /// </summary>
        public Core.IGPRTTStats? KcpRTTStats => dataPlaneTransport?.RTTStats ?? KcpClient?.RTTStats;
        
        /// <summary>
        /// KCP连接是否活跃（基于心跳）
        /// </summary>
        public bool IsKcpAlive => dataPlaneTransport?.IsAlive ?? (KcpClient?.IsAlive ?? false);

        /// <summary>
        /// KCP心跳间隔（秒）
        /// </summary>
        public float KcpHeartbeatInterval
        {
            get => kcpHeartbeatInterval;
            set 
            {
                kcpHeartbeatInterval = value;
                if (dataPlaneTransport != null)
                {
                    dataPlaneTransport.SetHeartbeatInterval(value);
                }
                else if (KcpClient != null && enableKcpHeartbeat)
                {
                    KcpClient.HeartbeatInterval = value;
                }
            }
        }

        /// <summary>
        /// 是否启用自动KCP心跳
        /// </summary>
        public bool EnableKcpHeartbeat
        {
            get => enableKcpHeartbeat;
            set
            {
                enableKcpHeartbeat = value;
                if (dataPlaneTransport != null)
                {
                    if (value) dataPlaneTransport.StartHeartbeat(kcpHeartbeatInterval);
                    else dataPlaneTransport.StopHeartbeat();
                }
                else if (KcpClient != null)
                {
                    if (value) KcpClient.StartHeartbeat(kcpHeartbeatInterval);
                    else KcpClient.StopHeartbeat();
                }
            }
        }

        /// <summary>
        /// 是否显示RTT统计（OnGUI）
        /// </summary>
        public bool ShowRTTStats
        {
            get => showRTTStats;
            set => showRTTStats = value;
        }

        /// <summary>
        /// 授权验证失败时是否显示 SDK 自带的保底提示。
        /// </summary>
        public bool ShowAuthorizationFailureFallback
        {
            get => showAuthorizationFailureFallback;
            set => showAuthorizationFailureFallback = value;
        }

        /// <summary>
        /// 授权验证失败保底提示标题。
        /// </summary>
        public string AuthorizationFailureFallbackTitle
        {
            get => authorizationFailureFallbackTitle;
            set => authorizationFailureFallbackTitle = value ?? string.Empty;
        }

        /// <summary>
        /// 授权验证失败保底提示正文。留空时显示最近一次失败原因。
        /// </summary>
        public string AuthorizationFailureFallbackMessage
        {
            get => authorizationFailureFallbackMessage;
            set => authorizationFailureFallbackMessage = value ?? string.Empty;
        }

        /// <summary>
        /// 授权验证失败保底提示引导语。
        /// </summary>
        public string AuthorizationFailureFallbackHint
        {
            get => authorizationFailureFallbackHint;
            set => authorizationFailureFallbackHint = value ?? string.Empty;
        }

        /// <summary>
        /// 授权验证失败保底提示按钮文案。
        /// </summary>
        public string AuthorizationFailureFallbackButtonText
        {
            get => authorizationFailureFallbackButtonText;
            set => authorizationFailureFallbackButtonText = value ?? string.Empty;
        }

        /// <summary>
        /// 当前是否正在显示授权验证失败保底提示。
        /// </summary>
        public bool IsAuthorizationFailureFallbackVisible =>
            showAuthorizationFailureFallback &&
            !authorizationFailureFallbackDismissed &&
            authorizationState == IGPAuthorizationState.Failed &&
            !string.IsNullOrWhiteSpace(GetAuthorizationFailureDisplayMessage());

        private bool DesktopSessionAutoAttach => config == null || config.desktopSessionAutoAttach;

        private string DesktopPipeEndpoint => IGPDesktopSessionEnvironment.ResolvePipeEndpoint(
            config?.desktopPipeEndpoint,
            SdkEnvironment);

        private void EnsureSDKInitialized()
        {
            if (!initializeRequested)
            {
                throw new IGPSDKException("IGP SDK is not initialized. Call IGPRuntimeManager.InitializeAsync() before using SDK features.");
            }
        }
        
        #endregion
        
        #region Unity Lifecycle

#if UNITY_EDITOR
        private const string DefaultConfigAssetPath = "Assets/IGP/IGPConfig.asset";

        private void Reset()
        {
            AutoAssignConfigInEditor();
        }

        private void OnValidate()
        {
            if (config != null || Application.isPlaying || EditorApplication.isCompiling || EditorApplication.isUpdating)
            {
                return;
            }

            EditorApplication.delayCall -= AutoAssignConfigInEditor;
            EditorApplication.delayCall += AutoAssignConfigInEditor;
        }

        private void AutoAssignConfigInEditor()
        {
            if (this == null || config != null || Application.isPlaying)
            {
                return;
            }

            var resolvedConfig = ResolveConfigAssetInEditor();
            if (resolvedConfig == null)
            {
                return;
            }

            config = resolvedConfig;
            EditorUtility.SetDirty(this);
        }

        private static IGPConfig? ResolveConfigAssetInEditor()
        {
            var defaultConfig = AssetDatabase.LoadAssetAtPath<IGPConfig>(DefaultConfigAssetPath);
            if (defaultConfig != null)
            {
                return defaultConfig;
            }

            var guids = AssetDatabase.FindAssets("t:IGPConfig");
            if (guids.Length == 1)
            {
                return AssetDatabase.LoadAssetAtPath<IGPConfig>(AssetDatabase.GUIDToAssetPath(guids[0]));
            }

            if (guids.Length > 1)
            {
                Array.Sort(guids, StringComparer.Ordinal);
                var firstPath = AssetDatabase.GUIDToAssetPath(guids[0]);
                Debug.LogWarning($"[IGP] Multiple IGPConfig assets found. Auto-assigned the first one: {firstPath}");
                return AssetDatabase.LoadAssetAtPath<IGPConfig>(firstPath);
            }

            EnsureDefaultConfigFolderInEditor();
            var createdConfig = ScriptableObject.CreateInstance<IGPConfig>();
            AssetDatabase.CreateAsset(createdConfig, DefaultConfigAssetPath);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            Debug.Log($"[IGP] Created default IGPConfig at {DefaultConfigAssetPath}");
            return createdConfig;
        }

        private static void EnsureDefaultConfigFolderInEditor()
        {
            if (!AssetDatabase.IsValidFolder("Assets/IGP"))
            {
                AssetDatabase.CreateFolder("Assets", "IGP");
            }
        }
#endif
        
        private void Awake()
        {
            try
            {
                cancellationTokenSource ??= new CancellationTokenSource();
            }
            catch (Exception ex)
            {
                Debug.LogError($"[IGP] Awake failed: {ex.Message}");
                onError?.Invoke($"SDK prepare failed: {ex.Message}");
            }
        }

        /// <summary>
        /// 显式初始化 IGP SDK。游戏需要主动调用该方法；仅把 IGPRuntimeManager 放进场景不会自动启动 SDK。
        /// </summary>
        public async Task<bool> InitializeAsync()
        {
            if (sdkInitializationTask != null)
            {
                return await sdkInitializationTask;
            }

            initializeRequested = true;
            sdkInitializationTask = InitializeInternalAsync();
            return await sdkInitializationTask;
        }

        /// <summary>
        /// 显式初始化 IGP SDK 的 fire-and-forget 入口，便于 UnityEvent 或同步启动脚本调用。
        /// </summary>
        public void Initialize()
        {
            _ = RunInitializeAndReportAsync();
        }

        private async Task RunInitializeAndReportAsync()
        {
            try
            {
                await InitializeAsync();
            }
            catch (Exception ex)
            {
                Debug.LogError($"[IGP] Initialize failed: {ex.Message}");
                onError?.Invoke($"SDK initialization failed: {ex.Message}");
            }
        }

        private async Task<bool> InitializeInternalAsync()
        {
            try
            {
                cancellationTokenSource ??= new CancellationTokenSource();

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

                Network = new IGPNetwork(this);
                InitializeSDKClients();

                if (heartbeatCoroutine == null)
                {
                    heartbeatCoroutine = StartCoroutine(HeartbeatCoroutine());
                }

                if (string.IsNullOrEmpty(playerId))
                {
                    playerId = GeneratePlayerId();
                }

                if (string.IsNullOrEmpty(playerName))
                {
                    playerName = "Player_" + UnityEngine.Random.Range(1000, 9999);
                }

                if (ShouldAutoAttachDesktopSessionOnStartup())
                {
                    var attached = await TryAttachDesktopSessionAsync();
                    if (!attached)
                    {
                        Debug.Log("[IGP] Desktop session is unavailable; attempting to launch desktop and retry attach.");
                        await TryLaunchDesktopAndRetryAttachAsync();
                    }
                }

                if (!await TryAuthorizeOnStartupAsync())
                {
                    return false;
                }

                if (HasHostedLaunchContext(pendingLaunchOptions))
                {
                    if (!await TryBootstrapFromLaunchTicketAsync())
                    {
                        onError?.Invoke("Launch bootstrap failed: hosted session endpoint is unavailable");
                        return false;
                    }
                }
                else if (AutoConnect && !string.IsNullOrEmpty(currentRoomId))
                {
                    onError?.Invoke("AutoConnect requires a desktop-provided room launch context");
                }

                sdkInitialized = true;
                Debug.Log($"[IGP] Initialized with PlayerID: {playerId}, PlayerName: {playerName}");
                return true;
            }
            catch (Exception ex)
            {
                sdkInitialized = false;
                Debug.LogError($"[IGP] Initialize failed: {ex.Message}");
                onError?.Invoke($"SDK initialization failed: {ex.Message}");
                return false;
            }
        }
        
        private void InitializeSDKClients()
        {
            try
            {
                WebSocketClient?.Dispose();
                KcpClient?.Dispose();

                // 初始化宿主 WebSocket façade（真实传输由 desktop host session 接管）
                WebSocketClient = new IGPHostedRealtimeClient();
                
                // 设置WebSocket事件
                WebSocketClient.ConnectionStateChanged += (connected) =>
                {
                    if (isDestroyed) return;
                    onConnectionStateChanged?.Invoke(connected);
                };

                WebSocketClient.MessageReceived += (message) =>
                {
                    if (isDestroyed) return;

                    // Intercept P2P data for the Network module
                    if (message.type == "p2p_data")
                    {
                        Network?.HandleIncomingP2P(message.content);
                        return; // Stop further propagation for P2P data
                    }

                    // 处理队伍变更消息
                    if (message.type == "team_change")
                    {
                        onTeamChanged?.Invoke(message.type, message.content);
                        return; // 队伍消息有专门事件，不传播到通用事件
                    }

                    // 处理队伍初始化消息
                    if (message.type == "team_initialize")
                    {
                        onTeamInitialized?.Invoke(message.type, message.content);
                        return;
                    }

                    // 处理队伍状态更新消息
                    if (message.type == "team_status")
                    {
                        onTeamStatusUpdated?.Invoke(message.type, message.content);
                        return;
                    }

                    // 处理房主转移消息
                    if (message.type == "room_host_transfer")
                    {
                        var from = message.content?.GetType().GetProperty("from")?.GetValue(message.content)?.ToString();
                        var to = message.content?.GetType().GetProperty("to")?.GetValue(message.content)?.ToString();

                        if (!string.IsNullOrEmpty(to))
                        {
                            roomData.hostId = to; // 更新本地房主
                        }

                        onHostTransferred?.Invoke(from ?? string.Empty, to ?? string.Empty);
                        return;
                    }

                    onMessageReceived?.Invoke(message.type, message.content);
                };

                WebSocketClient.ErrorOccurred += (error) =>
                {
                    if (isDestroyed) return;
                    onError?.Invoke(error);
                };

                // 初始化KCP客户端（连接在进入房间后触发）
                KcpClient = new IGPKcpClient();
                KcpClient.MessageReceived += (message) =>
                {
                    if (isDestroyed) return;

                    // Intercept P2P data for the Network module
                    if (message.type == "p2p_data")
                    {
                        Network?.HandleIncomingP2P(message.content);
                        return;
                    }

                    onMessageReceived?.Invoke(message.type, message.content);
                };
                KcpClient.ErrorOccurred += (error) =>
                {
                    if (isDestroyed) return;
                    if (dataPlaneTransport != null || currentDataPlaneMode != IGPDataPlaneMode.None)
                    {
                        currentHostedDataPlaneStatus = "error";
                        currentHostedDataPlaneError = error;
                    }
                    onError?.Invoke(error);
                };
                KcpClient.ConnectionStateChanged += (connected) =>
                {
                    if (isDestroyed) return;
                    HandleKcpConnectionStateChanged(connected);
                };
                
                Debug.Log("[IGP] SDK clients initialized successfully");
            }
            catch (Exception ex)
            {
                Debug.LogError($"[IGP] Failed to initialize SDK clients: {ex.Message}");
                onError?.Invoke($"SDK initialization failed: {ex.Message}");
            }
        }
        
        private void OnDestroy()
        {
            isDestroyed = true;  // 设置销毁标志，防止事件回调
            StopAuthorizationRefreshLoop();
            Disconnect();
            
            // 清理Network模块
            Network?.Dispose();
            
            // 清理资源
            if (cancellationTokenSource != null)
            {
                cancellationTokenSource.Cancel();
                cancellationTokenSource.Dispose();
                cancellationTokenSource = null;
            }
            
            if (heartbeatCoroutine != null)
            {
                StopCoroutine(heartbeatCoroutine);
                heartbeatCoroutine = null;
            }
            
            // 清理SDK客户端
            desktopSessionClient?.Dispose();
            WebSocketClient?.Dispose();
            KcpClient?.Dispose();
        }

        private void Update()
        {
            // drive Network module update (RTT broadcasting)
            Network?.Update();

            // drive KCP tick on main thread
            if (dataPlaneTransport != null)
            {
                dataPlaneTransport.Tick();
            }

            // 对齐 GameMaker bridge 的 TryRecoverDesktopAttachIfNeeded / TryRefreshAuthorizationStateIfNeeded
            // 在主循环内持续做 desktop session + authorization 自愈。
            TryRecoverDesktopAttachIfNeeded();
            TryRecoverAuthorizationStateIfNeeded();
        }

        private void TryRecoverDesktopAttachIfNeeded()
        {
            if (isDestroyed || !DesktopSessionAutoAttach)
            {
                return;
            }

            if (desktopAttachRetryInFlight)
            {
                return;
            }

            if (!nextDesktopAttachRetryAtUtc.HasValue ||
                DateTime.UtcNow < nextDesktopAttachRetryAtUtc.Value)
            {
                return;
            }

            if (IsDesktopSessionAttached)
            {
                nextDesktopAttachRetryAtUtc = null;
                return;
            }

            if (!CanRetryDesktopAttach())
            {
                nextDesktopAttachRetryAtUtc = null;
                return;
            }

            desktopAttachRetryInFlight = true;
            nextDesktopAttachRetryAtUtc = null;
            _ = RunDesktopAttachRetryAsync();
        }

        private async Task RunDesktopAttachRetryAsync()
        {
            try
            {
                var attached = await TryAttachDesktopSessionAsync();
                if (!attached && !isDestroyed && CanRetryDesktopAttach())
                {
                    // 失败则安排下一轮退避重试，避免死循环但保证持续自愈。
                    nextDesktopAttachRetryAtUtc = DateTime.UtcNow.AddSeconds(DesktopAttachRetryBackoffSeconds);
                }
                else if (attached)
                {
                    // attach 回到正常之后，立刻排程一次授权恢复。
                    ScheduleAuthorizationRecoverIfNeeded();
                }
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[IGP] Desktop attach recover failed: {ex.Message}");
                if (!isDestroyed && CanRetryDesktopAttach())
                {
                    nextDesktopAttachRetryAtUtc = DateTime.UtcNow.AddSeconds(DesktopAttachRetryBackoffSeconds);
                }
            }
            finally
            {
                desktopAttachRetryInFlight = false;
            }
        }

        private void TryRecoverAuthorizationStateIfNeeded()
        {
            if (isDestroyed)
            {
                return;
            }

            if (authorizationRecoverInFlight)
            {
                return;
            }

            if (!nextAuthorizationRecoverAtUtc.HasValue ||
                DateTime.UtcNow < nextAuthorizationRecoverAtUtc.Value)
            {
                return;
            }

            // 已在线或被跳过则不再自愈。
            if (authorizationState == IGPAuthorizationState.AuthorizedOnline ||
                authorizationState == IGPAuthorizationState.Skipped)
            {
                nextAuthorizationRecoverAtUtc = null;
                return;
            }

            // desktop session 未就绪时跳过，由 desktop attach 自愈链路驱动。
            if (!IsDesktopSessionAttached)
            {
                nextAuthorizationRecoverAtUtc = null;
                return;
            }

            authorizationRecoverInFlight = true;
            nextAuthorizationRecoverAtUtc = null;
            _ = RunAuthorizationRecoverAsync();
        }

        private async Task RunAuthorizationRecoverAsync()
        {
            try
            {
                var ok = await TryAuthorizeOnStartupAsync();
                if (!ok && !isDestroyed &&
                    authorizationState != IGPAuthorizationState.AuthorizedOnline &&
                    authorizationState != IGPAuthorizationState.Skipped)
                {
                    // 仍未恢复则安排下一次。
                    nextAuthorizationRecoverAtUtc = DateTime.UtcNow.AddSeconds(AuthorizationRecoverBackoffSeconds);
                }
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[IGP] Authorization recover failed: {ex.Message}");
                if (!isDestroyed &&
                    authorizationState != IGPAuthorizationState.AuthorizedOnline &&
                    authorizationState != IGPAuthorizationState.Skipped)
                {
                    nextAuthorizationRecoverAtUtc = DateTime.UtcNow.AddSeconds(AuthorizationRecoverBackoffSeconds);
                }
            }
            finally
            {
                authorizationRecoverInFlight = false;
            }
        }

        private void OnGUI()
        {
            if (showRTTStats && KcpRTTStats != null && KcpRTTStats.SampleCount > 0)
            {
                var stats = KcpRTTStats;
                GUI.Box(new Rect(10, 10, 180, 60), "IGP RTT");
                GUI.Label(new Rect(20, 30, 160, 20), $"Latency: {stats.AvgRTT * 1000:F0}ms");
                GUI.Label(new Rect(20, 45, 160, 20), $"Quality: {stats.GetQuality()}");
            }

            RenderAuthorizationFailureFallback();
        }
        
        #endregion
        
        #region Public Methods

        /// <summary>
        /// 手动关闭当前授权验证失败保底提示。
        /// </summary>
        public void DismissAuthorizationFailureFallback()
        {
            authorizationFailureFallbackDismissed = true;
        }
        
        private async Task ConnectHostedRealtimeAsync(string roomId, string? roomCode = null)
        {
            try
            {
                EnsureSDKInitialized();
                hostedDisconnectRequested = false;
                EnsureHostedServerUrl();
                currentRoomId = roomId;
                currentRoomCode = roomCode ?? string.Empty;
                
                Debug.Log($"[IGP] Connecting to room {roomId}...");

                if (WebSocketClient == null)
                {
                    throw new InvalidOperationException("WebSocket client not initialized");
                }

                if (!IsHostedSessionAttached)
                {
                    throw new IGPSDKException(
                        "Room context is required before hosted realtime can be used",
                        IGPErrorCodes.ERR_ROOM_CONTEXT_REQUIRED);
                }

                await WaitForHostedRealtimeConnectionAsync();

                await EnsureHostedDataPlaneAttachedAsync();
                
                Debug.Log($"[IGP] Successfully connected to room {roomId}");
            }
            catch (Exception ex)
            {
                string errorMessage = ex.Message;

                // 为409冲突错误提供更友好的中文提示
                if (ex.Message.Contains("409") || ex.Message.Contains("Conflict"))
                {
                    Debug.LogWarning($"[IGP] Room connection conflict for {roomId}");

                    // 检查是否已经在房间中
                    if (!string.IsNullOrEmpty(currentRoomId) && currentRoomId == roomId)
                    {
                        errorMessage = "您已经连接到这个房间了！";
                        Debug.Log("[IGP] Already connected to this room, skipping connection.");
                        onRoomJoined?.Invoke(roomData); // 触发房间加入事件
                        return; // 不是真正的错误，直接返回
                    }
                    else if (!string.IsNullOrEmpty(currentRoomId))
                    {
                        errorMessage = $"您已经在房间 {currentRoomId} 中！请先离开当前房间再加入新房间。";
                        Debug.LogWarning($"[IGP] {errorMessage}");
                    }
                }

                Debug.LogError($"[IGP] Failed to connect to room {roomId}: {errorMessage}");
                onError?.Invoke($"Connection failed: {errorMessage}");
                throw;
            }
        }

        private async Task WaitForHostedRealtimeConnectionAsync()
        {
            if (WebSocketClient == null)
            {
                throw new IGPSDKException("WebSocket client not initialized");
            }

            if (WebSocketClient.IsConnected)
            {
                return;
            }

            var readyTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

            void HandleConnectionStateChanged(bool connected)
            {
                if (connected)
                {
                    readyTcs.TrySetResult(true);
                }
            }

            WebSocketClient.ConnectionStateChanged += HandleConnectionStateChanged;
            try
            {
                if (WebSocketClient.IsConnected)
                {
                    return;
                }

                var completed = await Task.WhenAny(
                    readyTcs.Task,
                    Task.Delay(HostedRealtimeReadyTimeout, RuntimeCancellationToken));

                if (completed == readyTcs.Task && WebSocketClient.IsConnected)
                {
                    return;
                }

                RuntimeCancellationToken.ThrowIfCancellationRequested();

                throw new IGPSDKException(
                    "Curio desktop hosted realtime connection is not ready yet. " +
                    $"sessionAttached={IsHostedSessionAttached}, " +
                    $"connectionState={hostedConnectionStatus}, " +
                    $"roomId={currentRoomId}, " +
                    $"waited={HostedRealtimeReadyTimeout.TotalSeconds:F0}s");
            }
            finally
            {
                WebSocketClient.ConnectionStateChanged -= HandleConnectionStateChanged;
            }
        }

        private void EnsureHostedServerUrl()
        {
            if (!string.IsNullOrWhiteSpace(serverUrl))
            {
                return;
            }

            throw new IGPSDKException(
                "Hosted bootstrap did not provide a server endpoint",
                IGPErrorCodes.ERR_ROOM_CONTEXT_REQUIRED);
        }

        /// <summary>
        /// 通过宿主控制通道设置准备状态。
        /// </summary>
        public async Task SetReadyAsync(bool isReady)
        {
            var session = EnsureHostedSession();
            await session.SetReadyAsync(isReady, RuntimeCancellationToken);
        }

        /// <summary>
        /// 通过宿主控制通道离开当前房间。
        /// </summary>
        public async Task LeaveHostedRoomAsync()
        {
            var session = EnsureHostedSession();
            await session.LeaveRoomAsync(RuntimeCancellationToken);
            await DisconnectAsync();
        }

        /// <summary>
        /// 通过宿主控制通道开始游戏。
        /// </summary>
        public async Task StartHostedGameAsync()
        {
            var session = EnsureHostedSession();
            await session.StartGameAsync(RuntimeCancellationToken);
        }

        /// <summary>
        /// 通过宿主控制通道结束当前对局。
        /// </summary>
        public async Task FinishHostedGameAsync()
        {
            var session = EnsureHostedSession();
            await session.FinishGameAsync(RuntimeCancellationToken);
        }

        /// <summary>
        /// 通过宿主控制通道请求再来一局。
        /// </summary>
        public async Task RequestRematchAsync()
        {
            var session = EnsureHostedSession();
            await session.RematchGameAsync(RuntimeCancellationToken);
        }

        /// <summary>
        /// 通过宿主控制通道切换当前玩家队伍。
        /// </summary>
        public async Task ChangeHostedTeamAsync(string teamId)
        {
            var session = EnsureHostedSession();
            await session.ChangeTeamAsync(teamId, RuntimeCancellationToken);
        }

        /// <summary>
        /// 主动刷新宿主房间快照。
        /// </summary>
        public async Task RefreshHostedRoomAsync()
        {
            var session = EnsureHostedSession();
            await session.RefreshRoomAsync(RuntimeCancellationToken);
        }

        /// <summary>
        /// 上报一次性成就解锁事件。
        /// </summary>
        public async Task<AchievementReportResult> UnlockAchievementAsync(
            string achievementKey,
            string? eventId = null,
            long? occurredAtUnixMs = null,
            int? appId = null)
        {
            var session = await EnsureDesktopSessionAsync(appId);
            EnsureDesktopUserContextAvailable();
            EnsureDesktopCapability(
                currentDesktopCapabilities?.achievements ?? false,
                "Achievements are not available on the current desktop session");
            var result = await session.UnlockAchievementAsync(
                achievementKey,
                eventId,
                occurredAtUnixMs,
                appId,
                RuntimeCancellationToken);
            return ParseDesktopCommandPayload(
                result,
                () => new AchievementReportResult
                {
                    success = true,
                    message = result.Message ?? string.Empty,
                });
        }

        /// <summary>
        /// 上报进度型成就当前进度。
        /// </summary>
        public async Task<AchievementReportResult> ReportAchievementProgressAsync(
            string achievementKey,
            double progressValue,
            string? progressSourceKey = null,
            string? eventId = null,
            long? occurredAtUnixMs = null,
            int? appId = null)
        {
            var session = await EnsureDesktopSessionAsync(appId);
            EnsureDesktopUserContextAvailable();
            EnsureDesktopCapability(
                currentDesktopCapabilities?.achievements ?? false,
                "Achievements are not available on the current desktop session");
            var result = await session.ReportAchievementProgressAsync(
                achievementKey,
                progressValue,
                progressSourceKey,
                eventId,
                occurredAtUnixMs,
                appId,
                RuntimeCancellationToken);
            return ParseDesktopCommandPayload(
                result,
                () => new AchievementReportResult
                {
                    success = true,
                    message = result.Message ?? string.Empty,
                });
        }

        /// <summary>
        /// 清除当前游戏的全部成就。
        /// </summary>
        public async Task<AchievementClearResult> ClearAchievementsAsync(int? appId = null)
        {
            var session = await EnsureDesktopSessionAsync(appId);
            EnsureDesktopUserContextAvailable();
            EnsureDesktopCapability(
                currentDesktopCapabilities?.achievements ?? false,
                "Achievements are not available on the current desktop session");
            var result = await session.ClearAchievementsAsync(appId, RuntimeCancellationToken);
            return ParseDesktopCommandPayload(
                result,
                () => new AchievementClearResult
                {
                    success = true,
                    message = result.Message ?? string.Empty,
                });
        }

        /// <summary>
        /// 获取当前 desktop 登录用户头像元信息。
        /// </summary>
        public async Task<IGPDesktopUserAvatarInfo> GetDesktopUserAvatarInfoAsync(int? appId = null)
        {
            var session = await EnsureDesktopSessionAsync(appId);
            EnsureDesktopUserContextAvailable();

            if (currentDesktopUserProfile != null && !currentDesktopUserProfile.avatarAvailable)
            {
                return BuildUnavailableDesktopUserAvatarInfo();
            }

            var info = await session.GetDesktopUserAvatarInfoAsync(RuntimeCancellationToken);
            UpdateCurrentDesktopUserAvatarInfo(info);
            return info;
        }

        /// <summary>
        /// 获取当前 desktop 登录用户头像 PNG 数据。
        /// </summary>
        public async Task<IGPDesktopUserAvatarImage> GetDesktopUserAvatarAsync(int? appId = null)
        {
            var session = await EnsureDesktopSessionAsync(appId);
            EnsureDesktopUserContextAvailable();

            if (currentDesktopUserProfile != null && !currentDesktopUserProfile.avatarAvailable)
            {
                return BuildUnavailableDesktopUserAvatarImage();
            }

            var image = await session.GetDesktopUserAvatarAsync(RuntimeCancellationToken);
            UpdateCurrentDesktopUserAvatarInfo(image.info);
            if (!image.info.available)
            {
                image.pngBytes = Array.Empty<byte>();
            }

            return image;
        }

        internal async Task<bool> TryAttachDesktopSessionAsync(int? appIdOverride = null)
        {
            if (!initializeRequested)
            {
                MarkDesktopSessionUnavailable(
                    IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED,
                    "IGP SDK is not initialized");
                return false;
            }

            try
            {
                await AttachDesktopSessionAsync(appIdOverride);
                desktopSessionLastErrorCode = string.Empty;
                desktopSessionLastErrorMessage = string.Empty;
                return true;
            }
            catch (Exception ex)
            {
                CaptureDesktopAttachFailure(ex);
                Debug.LogWarning($"[IGP] Desktop session attach failed: {desktopSessionLastErrorMessage}");
                return false;
            }
        }

        internal async Task<bool> TryLaunchDesktopAndRetryAttachAsync(int? appIdOverride = null)
        {
            if (!initializeRequested)
            {
                MarkDesktopSessionUnavailable(
                    IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED,
                    "IGP SDK is not initialized");
                return false;
            }

            if (!CanRetryDesktopAttach())
            {
                return false;
            }

            var launchCommand = IGPDesktopSessionEnvironment.ResolveDesktopLaunchCommand(
                config?.desktopLaunchCommand,
                sdkEnvironment: SdkEnvironment);
            if (string.IsNullOrWhiteSpace(launchCommand))
            {
                MarkDesktopSessionUnavailable(
                    IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED,
                    "Desktop launch command could not be resolved");
                Debug.LogWarning("[IGP] Desktop launch retry skipped because the desktop launch command could not be resolved.");
                return false;
            }

            try
            {
                var startInfo = IGPDesktopSessionEnvironment.CreateDesktopLaunchStartInfo(launchCommand);
                Debug.Log($"[IGP] Launching desktop with command: {startInfo.FileName} {startInfo.Arguments}".TrimEnd());
                Process.Start(startInfo);
            }
            catch (Exception ex)
            {
                MarkDesktopSessionUnavailable(
                    IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED,
                    $"Failed to launch desktop: {ex.Message}");
                Debug.LogWarning($"[IGP] Desktop launch failed: {ex.Message}");
                return false;
            }

            for (var attempt = 0; attempt < 10; attempt++)
            {
                await Task.Delay(500, RuntimeCancellationToken);
                if (await TryAttachDesktopSessionAsync(appIdOverride))
                {
                    Debug.Log("[IGP] Desktop launch retry succeeded and desktop session is now attached.");
                    return true;
                }

                if (!CanRetryDesktopAttach())
                {
                    Debug.LogWarning($"[IGP] Desktop launch retry stopped early: {desktopSessionLastErrorMessage}");
                    return false;
                }
            }

            if (string.IsNullOrWhiteSpace(desktopSessionLastErrorCode))
            {
                MarkDesktopSessionUnavailable(
                    IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED,
                    "Desktop session is unavailable after launch retry");
            }

            Debug.LogWarning($"[IGP] Desktop launch retry timed out: {desktopSessionLastErrorMessage}");

            return false;
        }

        private async Task AttachDesktopSessionAsync(int? appIdOverride = null)
        {
            var request = BuildDesktopAttachRequest(appIdOverride);

            if (desktopSessionClient != null &&
                desktopSessionClient.IsAttached &&
                desktopSessionClient.CurrentSession != null &&
                desktopSessionClient.CurrentSession.AppId == request.AppId)
            {
                return;
            }

            await DetachDesktopSessionAsync();

            var client = new IGPDesktopSessionClient(DesktopPipeEndpoint);
            client.Detached += HandleDesktopSessionDetached;
            client.ErrorOccurred += HandleDesktopSessionError;
            client.ErrorDetailed += HandleDesktopSessionErrorDetailed;
            client.AntiAddictionChanged += HandleDesktopAntiAddictionChanged;
            client.HostedBootstrapAvailable += HandleDesktopHostedBootstrapAvailable;

            try
            {
                var response = await client.ConnectAsync(request, RuntimeCancellationToken);
                desktopSessionClient = client;
                currentDesktopUserContext = new IGPDesktopUserContext
                {
                    userId = response.UserId,
                    accountId = response.AccountId,
                    loginState = response.LoginState,
                };
                currentDesktopCapabilities = response.Capabilities ?? new IGPDesktopCapabilitySet();
                currentDesktopUserProfile = response.UserProfile;
                currentAntiAddictionStatus = response.AntiAddictionStatus;
                desktopSessionLastErrorCode = string.Empty;
                desktopSessionLastErrorMessage = string.Empty;
                desktopSessionLastErrorCategory = string.Empty;
                desktopSessionLastChannelState = response.ChannelState ?? string.Empty;
                desktopSessionLastAttachState = response.AttachState ?? string.Empty;
                desktopSessionLastAttachSource = response.AttachSource ?? string.Empty;
                // attach 成功后清空 pending retry，避免 Update 再次触发。
                nextDesktopAttachRetryAtUtc = null;
            }
            catch
            {
                client.Detached -= HandleDesktopSessionDetached;
                client.ErrorOccurred -= HandleDesktopSessionError;
                client.ErrorDetailed -= HandleDesktopSessionErrorDetailed;
                client.AntiAddictionChanged -= HandleDesktopAntiAddictionChanged;
                client.HostedBootstrapAvailable -= HandleDesktopHostedBootstrapAvailable;
                client.Dispose();
                throw;
            }
        }

        private async Task DetachDesktopSessionAsync()
        {
            if (desktopSessionClient == null)
            {
                currentDesktopUserContext = null;
                currentDesktopCapabilities = null;
                currentDesktopUserProfile = null;
                currentAntiAddictionStatus = null;
                return;
            }

            var client = desktopSessionClient;
            desktopSessionClient = null;
            currentDesktopUserContext = null;
            currentDesktopCapabilities = null;
            currentDesktopUserProfile = null;
            currentAntiAddictionStatus = null;

            client.Detached -= HandleDesktopSessionDetached;
            client.ErrorOccurred -= HandleDesktopSessionError;
            client.ErrorDetailed -= HandleDesktopSessionErrorDetailed;
            client.AntiAddictionChanged -= HandleDesktopAntiAddictionChanged;
            client.HostedBootstrapAvailable -= HandleDesktopHostedBootstrapAvailable;
            await client.DisconnectAsync();
        }

        public async Task<IGPAntiAddictionStatus> RefreshAntiAddictionStatusAsync(int? appIdOverride = null)
        {
            var appId = ResolveDesktopAppId(appIdOverride);
            if (!appId.HasValue)
            {
                throw new IGPSDKException("IGP appId is required", IGPErrorCodes.ERR_APP_ID_REQUIRED);
            }

            var session = await EnsureDesktopSessionAsync(appId.Value);
            var status = await session.GetDesktopAntiAddictionStatusAsync(RuntimeCancellationToken);
            currentAntiAddictionStatus = status;
            onAntiAddictionStateChanged?.Invoke(status);
            return status;
        }

        public async Task<IGPAntiAddictionRealNameWebSession> CreateAntiAddictionRealNameWebSessionAsync(int? appIdOverride = null)
        {
            var appId = ResolveDesktopAppId(appIdOverride);
            if (!appId.HasValue)
            {
                throw new IGPSDKException("IGP appId is required", IGPErrorCodes.ERR_APP_ID_REQUIRED);
            }

            var session = await EnsureDesktopSessionAsync(appId.Value);
            return await session.CreateDesktopAntiAddictionRealNameUrlAsync(
                appId.Value,
                RuntimeCancellationToken);
        }

        public async Task<IGPDesktopSessionCommandResult> ForwardDesktopSessionCommandAsync(
            string command,
            string contentJson,
            int? appIdOverride = null)
        {
            if (string.IsNullOrWhiteSpace(command))
            {
                throw new ArgumentException("Desktop session command is required", nameof(command));
            }

            var session = await EnsureDesktopSessionAsync(appIdOverride);
            EnsureDesktopUserContextAvailable();
            return await session.ForwardNamedCommandAsync(
                command,
                contentJson ?? string.Empty,
                RuntimeCancellationToken);
        }

        private async Task<IGPDesktopSessionClient> EnsureDesktopSessionAsync(int? appIdOverride = null)
        {
            EnsureSDKInitialized();

            if (desktopSessionClient != null &&
                desktopSessionClient.IsAttached &&
                desktopSessionClient.CurrentSession != null &&
                (!appIdOverride.HasValue || desktopSessionClient.CurrentSession.AppId == appIdOverride.Value))
            {
                return desktopSessionClient;
            }

            if (await TryAttachDesktopSessionAsync(appIdOverride))
            {
                return desktopSessionClient!;
            }

            if (await TryLaunchDesktopAndRetryAttachAsync(appIdOverride))
            {
                return desktopSessionClient!;
            }

            throw CreateDesktopUnavailableException();
        }

        private bool ShouldAutoAttachDesktopSessionOnStartup()
        {
            if (!initializeRequested)
            {
                return false;
            }

            if (!DesktopSessionAutoAttach)
            {
                return false;
            }

            if (!Application.isEditor)
            {
                return true;
            }

            return true;
        }

        private IGPDesktopSessionAttachRequest BuildDesktopAttachRequest(int? appIdOverride)
        {
            var appId = ResolveDesktopAppId(appIdOverride);
            if (!appId.HasValue)
            {
                throw new IGPSDKException(
                    "IGP appId is required before desktop capabilities can be used",
                    IGPErrorCodes.ERR_APP_ID_REQUIRED);
            }

            var executablePath = IGPDesktopSessionEnvironment.ResolveExecutablePath(
                config?.desktopExecutablePathDebugOverride,
                editorDetector: () => Application.isEditor);
            if (string.IsNullOrWhiteSpace(executablePath))
            {
                throw new IGPSDKException(
                    "Executable path is required before desktop capabilities can be used",
                    IGPErrorCodes.ERR_EXECUTABLE_PATH_REQUIRED);
            }
            else if (Application.isEditor && string.IsNullOrWhiteSpace(config?.desktopExecutablePathDebugOverride))
            {
                Debug.Log("[IGP] Unity Editor desktop attach will use the Unity Editor executable path; desktop SDK debugging still validates appId and account permissions.");
            }

            return new IGPDesktopSessionAttachRequest
            {
                AppId = appId.Value,
                ExecutablePath = executablePath,
                ProcessId = GetCurrentProcessId(),
                SdkVersion = ResolveSdkVersion(),
                EngineVersion = Application.unityVersion,
            };
        }

        private int? ResolveDesktopAppId(int? appIdOverride)
        {
            if (appIdOverride.HasValue && appIdOverride.Value > 0)
            {
                return appIdOverride.Value;
            }

            return IGPDesktopSessionEnvironment.TryResolveAppId(
                config?.appId ?? 0,
                pendingLaunchOptions?.appId,
                out var resolvedAppId)
                ? resolvedAppId
                : null;
        }

        private int GetCurrentProcessId()
        {
            if (Application.isEditor)
            {
                return 0;
            }

            try
            {
                using var process = Process.GetCurrentProcess();
                return process.Id;
            }
            catch
            {
                return 0;
            }
        }

        private string ResolveSdkVersion()
        {
            return typeof(IGPRuntimeManager).Assembly.GetName().Version?.ToString() ?? "unknown";
        }

        private bool CanRetryDesktopAttach()
        {
            if (string.IsNullOrWhiteSpace(desktopSessionLastErrorCode))
            {
                return true;
            }

            // validation 类（appId / executable path 等）属于游戏配置错误，立刻告知用户而不是无限重试。
            if (IGPErrorCodes.IsValidationError(desktopSessionLastErrorCode))
            {
                return false;
            }

            // authorization 类同样不应该通过“重新 attach”自愈，需要上层走授权流程。
            if (IGPErrorCodes.IsAuthorizationError(desktopSessionLastErrorCode))
            {
                return false;
            }

            // channel / runtime / 以及经典的 DESKTOP_SESSION_REQUIRED 都允许继续重试。
            return true;
        }

        private void CaptureDesktopAttachFailure(Exception error)
        {
            if (error is IGPSDKException sdkException)
            {
                MarkDesktopSessionUnavailable(
                    string.IsNullOrWhiteSpace(sdkException.ErrorCode)
                        ? IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED
                        : sdkException.ErrorCode!,
                    sdkException.Message,
                    sdkException.Category,
                    sdkException.DesktopChannelState,
                    sdkException.DesktopAttachState);
                return;
            }

            MarkDesktopSessionUnavailable(
                IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED,
                error.Message,
                IGPErrorCodes.CATEGORY_CHANNEL,
                channelState: null,
                attachState: null);
        }

        private void MarkDesktopSessionUnavailable(string errorCode, string message)
        {
            MarkDesktopSessionUnavailable(
                errorCode,
                message,
                IGPErrorCodes.ResolveCategory(errorCode),
                channelState: null,
                attachState: null);
        }

        private void MarkDesktopSessionUnavailable(
            string errorCode,
            string message,
            string? category,
            string? channelState,
            string? attachState)
        {
            currentDesktopUserContext = null;
            currentDesktopCapabilities = null;
            currentDesktopUserProfile = null;
            currentAntiAddictionStatus = null;
            desktopSessionLastErrorCode = errorCode ?? string.Empty;
            desktopSessionLastErrorMessage = message ?? string.Empty;
            desktopSessionLastErrorCategory = string.IsNullOrWhiteSpace(category)
                ? IGPErrorCodes.ResolveCategory(errorCode)
                : category!;

            if (!string.IsNullOrEmpty(channelState))
            {
                desktopSessionLastChannelState = channelState!;
            }

            if (!string.IsNullOrEmpty(attachState))
            {
                desktopSessionLastAttachState = attachState!;
            }

            ScheduleDesktopAttachRetryIfNeeded();
        }

        private void ScheduleDesktopAttachRetryIfNeeded()
        {
            if (isDestroyed)
            {
                return;
            }

            if (!DesktopSessionAutoAttach)
            {
                nextDesktopAttachRetryAtUtc = null;
                return;
            }

            if (!CanRetryDesktopAttach())
            {
                nextDesktopAttachRetryAtUtc = null;
                return;
            }

            nextDesktopAttachRetryAtUtc = DateTime.UtcNow.AddSeconds(DesktopAttachRetryBackoffSeconds);
        }

        private void ScheduleAuthorizationRecoverIfNeeded()
        {
            if (isDestroyed)
            {
                return;
            }

            // 只有在 desktop session 可用、且当前不是已在线授权时，才安排自愈重试。
            if (authorizationState == IGPAuthorizationState.AuthorizedOnline ||
                authorizationState == IGPAuthorizationState.Skipped)
            {
                nextAuthorizationRecoverAtUtc = null;
                return;
            }

            nextAuthorizationRecoverAtUtc = DateTime.UtcNow.AddSeconds(AuthorizationRecoverBackoffSeconds);
        }

        private IGPDesktopUserAvatarInfo BuildUnavailableDesktopUserAvatarInfo()
        {
            return new IGPDesktopUserAvatarInfo
            {
                available = false,
                contentType = "image/png",
                width = 0,
                height = 0,
                encodedByteLength = 0,
                decodedByteLength = 0,
                avatarVersion = currentDesktopUserProfile?.avatarVersion ?? string.Empty,
            };
        }

        private IGPDesktopUserAvatarImage BuildUnavailableDesktopUserAvatarImage()
        {
            return new IGPDesktopUserAvatarImage
            {
                info = BuildUnavailableDesktopUserAvatarInfo(),
                pngBytes = Array.Empty<byte>(),
            };
        }

        private void UpdateCurrentDesktopUserAvatarInfo(IGPDesktopUserAvatarInfo? info)
        {
            if (info == null || currentDesktopUserProfile == null)
            {
                return;
            }

            currentDesktopUserProfile.avatarAvailable = info.available;
            if (!string.IsNullOrWhiteSpace(info.avatarVersion))
            {
                currentDesktopUserProfile.avatarVersion = info.avatarVersion;
            }
        }

        private IGPSDKException CreateDesktopUnavailableException()
        {
            var errorCode = string.IsNullOrWhiteSpace(desktopSessionLastErrorCode)
                ? IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED
                : desktopSessionLastErrorCode;
            var message = string.IsNullOrWhiteSpace(desktopSessionLastErrorMessage)
                ? "Desktop session is not attached"
                : desktopSessionLastErrorMessage;
            return new IGPSDKException(message, errorCode);
        }

        private void EnsureDesktopCapability(bool supported, string message)
        {
            if (!supported)
            {
                throw new IGPSDKException(
                    message,
                    IGPErrorCodes.ERR_DESKTOP_SESSION_CAPABILITY_MISSING);
            }
        }

        private void EnsureDesktopUserContextAvailable()
        {
            if (!(currentDesktopCapabilities?.userContext ?? false) ||
                currentDesktopUserContext == null ||
                string.IsNullOrWhiteSpace(currentDesktopUserContext.userId) ||
                !string.Equals(currentDesktopUserContext.loginState, "signedIn", StringComparison.OrdinalIgnoreCase))
            {
                throw new IGPSDKException(
                    "Desktop user context is required before desktop capabilities can be used",
                    IGPErrorCodes.ERR_DESKTOP_USER_CONTEXT_REQUIRED);
            }
        }

        private bool HasHostedLaunchContext(IGPLaunchOptions? launchOptions)
        {
            return launchOptions != null &&
                   !string.IsNullOrWhiteSpace(launchOptions.launchTicket) &&
                   !string.IsNullOrWhiteSpace(launchOptions.bootstrapEndpoint) &&
                   !string.IsNullOrWhiteSpace(launchOptions.bootstrapSecret);
        }

        private async Task<bool> TryAuthorizeOnStartupAsync()
        {
            ResetAuthorizationRuntimeState();
            SetAuthorizationState(IGPAuthorizationState.Pending);

            if (pendingLaunchOptions != null && !string.IsNullOrWhiteSpace(pendingLaunchOptions.ipcPipeName))
            {
                return await TryAuthorizeWithPipeAsync(pendingLaunchOptions.ipcPipeName);
            }

            var resolvedAppId = ResolveDesktopAppId(null);
            if (!resolvedAppId.HasValue)
            {
                ApplyAuthorizationSkipped();
                return true;
            }

            try
            {
                var session = await EnsureDesktopSessionAsync(resolvedAppId.Value);
                EnsureDesktopCapability(
                    currentDesktopCapabilities?.gameAuthorization ?? false,
                    "Authorization validation is not available on the current desktop session");

                var requestResult = await session.RequestGameAuthorizationAsync(
                    resolvedAppId.Value,
                    RuntimeCancellationToken);

                if (!requestResult.authorizationRequired)
                {
                    ApplyAuthorizationSkipped();
                    return true;
                }

                if (string.IsNullOrWhiteSpace(requestResult.pipeName))
                {
                    ApplyAuthorizationFailure(
                        IGPErrorCodes.ERR_AUTHORIZATION_PIPE_REQUIRED,
                        "Desktop did not return an authorization pipe");
                    return false;
                }

                return await TryAuthorizeWithPipeAsync(requestResult.pipeName);
            }
            catch (Exception ex)
            {
                ApplyAuthorizationFailure(
                    ex is IGPSDKException sdkEx && !string.IsNullOrWhiteSpace(sdkEx.ErrorCode)
                        ? sdkEx.ErrorCode!
                        : IGPErrorCodes.ERR_AUTHORIZATION_FAILED,
                    ex.Message);
                return false;
            }
        }

        private async Task<bool> TryAuthorizeWithPipeAsync(string pipeName)
        {
            if (string.IsNullOrWhiteSpace(pipeName))
            {
                ApplyAuthorizationFailure(
                    IGPErrorCodes.ERR_AUTHORIZATION_PIPE_REQUIRED,
                    "Authorization pipe is missing");
                return false;
            }

            try
            {
                authorizationPipeName = pipeName;
                var bootstrap = await ReadAuthorizationBootstrapAsync(pipeName, RuntimeCancellationToken);
                ApplyAuthorizationBootstrap(bootstrap);

                if (!isAuthorizationRequired || !bootstrap.autoAuth)
                {
                    ApplyAuthorizationSkipped();
                    return true;
                }

                if (bootstrap.offlineMode)
                {
                    if (TryAuthorizeOfflineFromCache())
                    {
                        return true;
                    }

                    ApplyAuthorizationFailure(
                        IGPErrorCodes.ERR_AUTHORIZATION_FAILED,
                        "Offline authorization failed because no valid local license was found");
                    return false;
                }

                if (string.IsNullOrWhiteSpace(authorizationBrokerToken))
                {
                    ApplyAuthorizationFailure(
                        IGPErrorCodes.ERR_AUTHORIZATION_FAILED,
                        "Authorization bootstrap is missing broker credentials");
                    return false;
                }

                var redeem = await RedeemAuthorizationPipeAsync(
                    authorizationPipeName,
                    authorizationBrokerToken,
                    ResolveSdkVersion(),
                    RuntimeCancellationToken);

                ApplyAuthorizationSession(redeem.sessionToken, redeem.expiresAt);
                SetAuthorizationState(IGPAuthorizationState.AuthorizedOnline);
                lastAuthorizationFailure = null;
                await TryUpdateOfflineLicenseCacheAsync(RuntimeCancellationToken);
                StartAuthorizationRefreshLoop(redeem.policy);
                return true;
            }
            catch (Exception ex)
            {
                if (TryAuthorizeOfflineFromCache())
                {
                    return true;
                }

                ApplyAuthorizationFailure(
                    IGPErrorCodes.ERR_AUTHORIZATION_FAILED,
                    $"Authorization validation failed: {ex.Message}");
                return false;
            }
        }

        private void ApplyAuthorizationBootstrap(IGPAuthorizationBootstrapPayload bootstrap)
        {
            isAuthorizationRequired = bootstrap.authorizationRequired;
            authorizationGameId = bootstrap.gameId > 0
                ? bootstrap.gameId
                : ResolveDesktopAppId(null) ?? 0;
            authorizationBrokerToken = bootstrap.brokerToken ?? string.Empty;
            authorizationDeviceIdHash = bootstrap.deviceIdHash ?? string.Empty;

            if (!string.IsNullOrWhiteSpace(bootstrap.offlineLicensePublicKey))
            {
                authorizationOfflineLicensePublicKey = bootstrap.offlineLicensePublicKey;
            }

            if (!string.IsNullOrWhiteSpace(bootstrap.offlineLicenseKeyId))
            {
                authorizationOfflineLicenseKeyId = bootstrap.offlineLicenseKeyId;
            }

            if (!string.IsNullOrWhiteSpace(bootstrap.offlineLicenseAlgorithm))
            {
                authorizationOfflineLicenseAlgorithm = bootstrap.offlineLicenseAlgorithm;
            }
        }

        private void ApplyAuthorizationSession(string sessionTokenValue, string expiresAt)
        {
            authorizationSessionToken = sessionTokenValue ?? string.Empty;
            authorizationSessionExpiresAt = TryParseIsoTimestamp(expiresAt);
        }

        private async Task TryUpdateOfflineLicenseCacheAsync(CancellationToken cancellationToken)
        {
            if (string.IsNullOrWhiteSpace(authorizationPipeName) ||
                string.IsNullOrWhiteSpace(authorizationBrokerToken) ||
                string.IsNullOrWhiteSpace(authorizationSessionToken))
            {
                return;
            }

            try
            {
                var response = await RequestOfflineLicensePipeAsync(
                    authorizationPipeName,
                    authorizationBrokerToken,
                    authorizationSessionToken,
                    ResolveSdkVersion(),
                    cancellationToken);

                var publicKey = ResolveAuthorizationPublicKeyForCache();
                if (string.IsNullOrWhiteSpace(publicKey))
                {
                    return;
                }

                VerifyOfflineLicense(
                    response.offlineLicense,
                    publicKey,
                    authorizationOfflineLicenseKeyId,
                    authorizationGameId,
                    authorizationDeviceIdHash);

                SaveOfflineLicenseCache(new IGPAuthorizationCacheRecord
                {
                    offlineLicense = response.offlineLicense,
                    expiresAt = response.expiresAt,
                    deviceIdHash = authorizationDeviceIdHash,
                    gameId = authorizationGameId,
                    publicKey = publicKey,
                    keyId = authorizationOfflineLicenseKeyId,
                    algorithm = authorizationOfflineLicenseAlgorithm,
                    savedAt = DateTime.UtcNow.ToString("O"),
                    clientVersion = ResolveSdkVersion(),
                });
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[IGP] Failed to update offline authorization cache: {ex.Message}");
            }
        }

        private void StartAuthorizationRefreshLoop(IGPAuthorizationPolicy? policy)
        {
            StopAuthorizationRefreshLoop();

            if (string.IsNullOrWhiteSpace(authorizationPipeName) ||
                string.IsNullOrWhiteSpace(authorizationBrokerToken) ||
                string.IsNullOrWhiteSpace(authorizationSessionToken))
            {
                return;
            }

            authorizationRefreshCts = CancellationTokenSource.CreateLinkedTokenSource(RuntimeCancellationToken);
            authorizationRefreshTask = Task.Run(async () =>
            {
                var token = authorizationRefreshCts.Token;
                while (!token.IsCancellationRequested &&
                       authorizationState == IGPAuthorizationState.AuthorizedOnline)
                {
                    var delay = ComputeAuthorizationRefreshDelay(policy);
                    if (delay > TimeSpan.Zero)
                    {
                        await Task.Delay(delay, token);
                    }

                    if (token.IsCancellationRequested ||
                        authorizationState != IGPAuthorizationState.AuthorizedOnline)
                    {
                        break;
                    }

                    var refreshed = await TryRefreshAuthorizationAsync(policy, token);
                    if (!refreshed)
                    {
                        break;
                    }
                }
            }, authorizationRefreshCts.Token);
        }

        private TimeSpan ComputeAuthorizationRefreshDelay(IGPAuthorizationPolicy? policy)
        {
            var refreshBeforeSeconds = policy?.refreshBeforeSeconds ?? 30;
            if (refreshBeforeSeconds < 5)
            {
                refreshBeforeSeconds = 5;
            }

            if (!authorizationSessionExpiresAt.HasValue)
            {
                return TimeSpan.FromSeconds(refreshBeforeSeconds);
            }

            var target = authorizationSessionExpiresAt.Value - TimeSpan.FromSeconds(refreshBeforeSeconds);
            var delay = target - DateTimeOffset.UtcNow;
            return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
        }

        private async Task<bool> TryRefreshAuthorizationAsync(
            IGPAuthorizationPolicy? policy,
            CancellationToken cancellationToken)
        {
            try
            {
                var refresh = await RefreshAuthorizationPipeAsync(
                    authorizationPipeName,
                    authorizationBrokerToken,
                    authorizationSessionToken,
                    ResolveSdkVersion(),
                    cancellationToken);

                ApplyAuthorizationSession(refresh.sessionToken, refresh.expiresAt);
                SetAuthorizationState(IGPAuthorizationState.AuthorizedOnline);
                await TryUpdateOfflineLicenseCacheAsync(cancellationToken);
                return true;
            }
            catch (Exception ex)
            {
                if (TryAuthorizeOfflineFromCache())
                {
                    return false;
                }

                ApplyAuthorizationFailure(
                    IGPErrorCodes.ERR_AUTHORIZATION_FAILED,
                    $"Authorization refresh failed: {ex.Message}");
                return false;
            }
        }

        private bool TryAuthorizeOfflineFromCache()
        {
            try
            {
                var cache = LoadOfflineLicenseCache();
                if (cache == null)
                {
                    return false;
                }

                var publicKey = !string.IsNullOrWhiteSpace(authorizationOfflineLicensePublicKey)
                    ? authorizationOfflineLicensePublicKey
                    : cache.publicKey ?? string.Empty;
                var keyId = !string.IsNullOrWhiteSpace(authorizationOfflineLicenseKeyId)
                    ? authorizationOfflineLicenseKeyId
                    : cache.keyId ?? string.Empty;

                VerifyOfflineLicense(
                    cache.offlineLicense,
                    publicKey,
                    keyId,
                    authorizationGameId > 0 ? authorizationGameId : cache.gameId,
                    !string.IsNullOrWhiteSpace(authorizationDeviceIdHash)
                        ? authorizationDeviceIdHash
                        : cache.deviceIdHash ?? string.Empty);

                authorizationOfflineLicensePublicKey = publicKey;
                authorizationOfflineLicenseKeyId = keyId;
                authorizationOfflineLicenseAlgorithm = !string.IsNullOrWhiteSpace(authorizationOfflineLicenseAlgorithm)
                    ? authorizationOfflineLicenseAlgorithm
                    : cache.algorithm ?? string.Empty;
                authorizationGameId = cache.gameId;
                authorizationDeviceIdHash = cache.deviceIdHash ?? string.Empty;
                SetAuthorizationState(IGPAuthorizationState.AuthorizedOffline);
                lastAuthorizationFailure = null;
                StopAuthorizationRefreshLoop();
                return true;
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"[IGP] Offline authorization cache is invalid: {ex.Message}");
                return false;
            }
        }

        private void ResetAuthorizationRuntimeState()
        {
            StopAuthorizationRefreshLoop();
            authorizationPipeName = string.Empty;
            authorizationBrokerToken = string.Empty;
            authorizationSessionToken = string.Empty;
            authorizationSessionExpiresAt = null;
            authorizationDeviceIdHash = string.Empty;
            authorizationGameId = 0;
            isAuthorizationRequired = false;
            lastAuthorizationFailure = null;
            authorizationFailureFallbackDismissed = false;
        }

        private void StopAuthorizationRefreshLoop()
        {
            if (authorizationRefreshCts == null)
            {
                return;
            }

            authorizationRefreshCts.Cancel();
            authorizationRefreshCts.Dispose();
            authorizationRefreshCts = null;
            authorizationRefreshTask = null;
        }

        private void ApplyAuthorizationSkipped()
        {
            isAuthorizationRequired = false;
            lastAuthorizationFailure = null;
            authorizationFailureFallbackDismissed = false;
            SetAuthorizationState(IGPAuthorizationState.Skipped);
            StopAuthorizationRefreshLoop();
        }

        private void ApplyAuthorizationFailure(string code, string message)
        {
            isAuthorizationRequired = true;
            lastAuthorizationFailure = new IGPAuthorizationFailureInfo
            {
                code = code ?? string.Empty,
                message = message ?? string.Empty,
            };
            authorizationFailureFallbackDismissed = false;
            StopAuthorizationRefreshLoop();
            SetAuthorizationState(IGPAuthorizationState.Failed);

            // 只有 non-validation 的授权失败值得自动重试；配置类错误（APP_ID 等）交给用户处理。
            if (!IGPErrorCodes.IsValidationError(code))
            {
                ScheduleAuthorizationRecoverIfNeeded();
            }

            if (!isDestroyed)
            {
                onAuthorizationFailed?.Invoke(message ?? string.Empty);
                onError?.Invoke(message ?? string.Empty);
            }
        }

        private void SetAuthorizationState(IGPAuthorizationState nextState)
        {
            authorizationState = nextState;
            if (nextState != IGPAuthorizationState.Failed)
            {
                authorizationFailureFallbackDismissed = false;
            }
            if (!isDestroyed)
            {
                onAuthorizationStateChanged?.Invoke(nextState);
            }
        }

        private void RenderAuthorizationFailureFallback()
        {
            if (!IsAuthorizationFailureFallbackVisible)
            {
                return;
            }

            var message = GetAuthorizationFailureDisplayMessage();
            var screenWidth = Screen.width > 0 ? Screen.width : 1280;
            var screenHeight = Screen.height > 0 ? Screen.height : 720;
            const float panelWidth = 560f;
            const float panelHeight = 230f;
            var panelX = Math.Max(20f, (screenWidth - panelWidth) * 0.5f);
            var panelY = Math.Max(20f, (screenHeight - panelHeight) * 0.5f);

            GUI.Box(new Rect(0, 0, screenWidth, screenHeight), string.Empty);
            GUI.Box(new Rect(panelX, panelY, panelWidth, panelHeight), "IGP Authorization");
            GUI.Label(new Rect(panelX + 20f, panelY + 36f, panelWidth - 40f, 20f), GetAuthorizationFailureFallbackTitle());
            GUI.Label(new Rect(panelX + 20f, panelY + 68f, panelWidth - 40f, 20f), message);
            GUI.Label(
                new Rect(panelX + 20f, panelY + 100f, panelWidth - 40f, 36f),
                GetAuthorizationFailureFallbackHint());

            if (GUI.Button(
                    new Rect(panelX + panelWidth - 120f, panelY + panelHeight - 44f, 100f, 24f),
                    GetAuthorizationFailureFallbackButtonText()))
            {
                DismissAuthorizationFailureFallback();
            }
        }

        private string GetAuthorizationFailureFallbackTitle()
        {
            if (!string.IsNullOrWhiteSpace(authorizationFailureFallbackTitle))
            {
                return authorizationFailureFallbackTitle;
            }

            return "Unable to verify game access";
        }

        private string GetAuthorizationFailureDisplayMessage()
        {
            if (!string.IsNullOrWhiteSpace(authorizationFailureFallbackMessage))
            {
                return authorizationFailureFallbackMessage;
            }

            if (!string.IsNullOrWhiteSpace(lastAuthorizationFailure?.message))
            {
                return lastAuthorizationFailure.message;
            }

            return "Authorization could not be completed.";
        }

        private string GetAuthorizationFailureFallbackHint()
        {
            if (!string.IsNullOrWhiteSpace(authorizationFailureFallbackHint))
            {
                return authorizationFailureFallbackHint;
            }

            return "Open IGP Desktop, make sure you are signed in, and then try again.";
        }

        private string GetAuthorizationFailureFallbackButtonText()
        {
            if (!string.IsNullOrWhiteSpace(authorizationFailureFallbackButtonText))
            {
                return authorizationFailureFallbackButtonText;
            }

            return "OK";
        }

        private string ResolveAuthorizationPublicKeyForCache()
        {
            if (!string.IsNullOrWhiteSpace(authorizationOfflineLicensePublicKey))
            {
                return authorizationOfflineLicensePublicKey;
            }

            var cache = LoadOfflineLicenseCache();
            return cache?.publicKey ?? string.Empty;
        }

        private string GetAuthorizationCachePath()
        {
            var appId = authorizationGameId > 0 ? authorizationGameId : ResolveDesktopAppId(null) ?? 0;
            if (appId <= 0)
            {
                throw new IGPSDKException(
                    "Authorization cache path is unavailable because appId is missing",
                    IGPErrorCodes.ERR_APP_ID_REQUIRED);
            }

            var root = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                "IndieGamesPass",
                "IGP.UnitySDK",
                "Authorization");
            Directory.CreateDirectory(root);
            return Path.Combine(root, $"app-{appId}.json");
        }

        private IGPAuthorizationCacheRecord? LoadOfflineLicenseCache()
        {
            var path = GetAuthorizationCachePath();
            if (!File.Exists(path))
            {
                return null;
            }

            var json = UnprotectAuthorizationCacheJson(File.ReadAllText(path, Encoding.UTF8));
            return JsonConvert.DeserializeObject<IGPAuthorizationCacheRecord>(json);
        }

        private void SaveOfflineLicenseCache(IGPAuthorizationCacheRecord record)
        {
            var json = JsonConvert.SerializeObject(record);
            File.WriteAllText(GetAuthorizationCachePath(), ProtectAuthorizationCacheJson(json), Encoding.UTF8);
        }

        private static string ProtectAuthorizationCacheJson(string json)
        {
            if (string.IsNullOrEmpty(json))
            {
                return json;
            }

            if (TryProtectLocalUserBytes(Encoding.UTF8.GetBytes(json), out var protectedBytes))
            {
                return JsonConvert.SerializeObject(new IGPAuthorizationCacheEnvelope
                {
                    version = 1,
                    protection = "windows-dpapi",
                    payload = Convert.ToBase64String(protectedBytes),
                });
            }

            return json;
        }

        private static string UnprotectAuthorizationCacheJson(string stored)
        {
            if (string.IsNullOrWhiteSpace(stored))
            {
                return stored;
            }

            IGPAuthorizationCacheEnvelope? envelope = null;
            try
            {
                envelope = JsonConvert.DeserializeObject<IGPAuthorizationCacheEnvelope>(stored);
            }
            catch
            {
                return stored;
            }

            if (envelope == null || string.IsNullOrWhiteSpace(envelope.protection))
            {
                return stored;
            }

            if (!string.Equals(envelope.protection, "windows-dpapi", StringComparison.OrdinalIgnoreCase))
            {
                throw new IGPSDKException("Unsupported authorization cache protection");
            }

            if (string.IsNullOrWhiteSpace(envelope.payload))
            {
                throw new IGPSDKException("Protected authorization cache is empty");
            }

            var protectedBytes = Convert.FromBase64String(envelope.payload);
            if (!TryUnprotectLocalUserBytes(protectedBytes, out var jsonBytes))
            {
                throw new IGPSDKException("Authorization cache could not be decrypted on this device");
            }

            return Encoding.UTF8.GetString(jsonBytes);
        }

        private static bool TryProtectLocalUserBytes(byte[] plainBytes, out byte[] protectedBytes)
        {
            protectedBytes = Array.Empty<byte>();
            if (!IsWindowsPlatform())
            {
                return false;
            }

            try
            {
                var protectedDataType = ResolveProtectedDataType();
                var scopeType = ResolveDataProtectionScopeType();
                if (protectedDataType == null || scopeType == null)
                {
                    return false;
                }

                var protect = protectedDataType.GetMethod("Protect", new[] { typeof(byte[]), typeof(byte[]), scopeType });
                if (protect == null)
                {
                    return false;
                }

                var currentUserScope = Enum.Parse(scopeType, "CurrentUser");
                if (protect.Invoke(null, new object?[] { plainBytes, null, currentUserScope }) is byte[] result &&
                    result.Length > 0)
                {
                    protectedBytes = result;
                    return true;
                }
            }
            catch
            {
            }

            return false;
        }

        private static bool TryUnprotectLocalUserBytes(byte[] protectedBytes, out byte[] plainBytes)
        {
            plainBytes = Array.Empty<byte>();
            if (!IsWindowsPlatform())
            {
                return false;
            }

            try
            {
                var protectedDataType = ResolveProtectedDataType();
                var scopeType = ResolveDataProtectionScopeType();
                if (protectedDataType == null || scopeType == null)
                {
                    return false;
                }

                var unprotect = protectedDataType.GetMethod("Unprotect", new[] { typeof(byte[]), typeof(byte[]), scopeType });
                if (unprotect == null)
                {
                    return false;
                }

                var currentUserScope = Enum.Parse(scopeType, "CurrentUser");
                if (unprotect.Invoke(null, new object?[] { protectedBytes, null, currentUserScope }) is byte[] result &&
                    result.Length > 0)
                {
                    plainBytes = result;
                    return true;
                }
            }
            catch
            {
            }

            return false;
        }

        private static Type? ResolveProtectedDataType()
        {
            return Type.GetType("System.Security.Cryptography.ProtectedData, System.Security.Cryptography.ProtectedData") ??
                   Type.GetType("System.Security.Cryptography.ProtectedData, System.Security.Cryptography");
        }

        private static Type? ResolveDataProtectionScopeType()
        {
            return Type.GetType("System.Security.Cryptography.DataProtectionScope, System.Security.Cryptography.ProtectedData") ??
                   Type.GetType("System.Security.Cryptography.DataProtectionScope, System.Security.Cryptography");
        }

        private static bool IsWindowsPlatform()
        {
            var platform = Environment.OSVersion.Platform;
            return platform == PlatformID.Win32NT ||
                   platform == PlatformID.Win32Windows ||
                   platform == PlatformID.Win32S ||
                   platform == PlatformID.WinCE;
        }

        private static DateTimeOffset? TryParseIsoTimestamp(string? value)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return null;
            }

            return DateTimeOffset.TryParse(value, out var parsed) ? parsed : null;
        }

        private static async Task<IGPAuthorizationBootstrapPayload> ReadAuthorizationBootstrapAsync(
            string pipeName,
            CancellationToken cancellationToken)
        {
            var response = await SendAuthorizationPipeRequestAsync<IGPAuthorizationBootstrapEnvelope>(
                pipeName,
                new IGPAuthorizationPipeRequest
                {
                    type = "getBootstrapContext",
                },
                cancellationToken);

            if (!response.ok || response.data == null)
            {
                throw new IGPSDKException(response.error ?? "Authorization bootstrap failed");
            }

            return response.data;
        }

        private static async Task<IGPAuthorizationSessionResponse> RedeemAuthorizationPipeAsync(
            string pipeName,
            string brokerToken,
            string clientVersion,
            CancellationToken cancellationToken)
        {
            var response = await SendAuthorizationPipeRequestAsync<IGPAuthorizationSessionEnvelope>(
                pipeName,
                new IGPAuthorizationPipeRequest
                {
                    type = "redeem",
                    authToken = brokerToken,
                    clientVersion = clientVersion,
                },
                cancellationToken);

            if (!response.ok || response.data == null)
            {
                throw new IGPSDKException(response.error ?? "Authorization redeem failed");
            }

            return response.data;
        }

        private static async Task<IGPAuthorizationSessionResponse> RefreshAuthorizationPipeAsync(
            string pipeName,
            string brokerToken,
            string sessionTokenValue,
            string clientVersion,
            CancellationToken cancellationToken)
        {
            var response = await SendAuthorizationPipeRequestAsync<IGPAuthorizationSessionEnvelope>(
                pipeName,
                new IGPAuthorizationPipeRequest
                {
                    type = "refresh",
                    authToken = brokerToken,
                    sessionToken = sessionTokenValue,
                    clientVersion = clientVersion,
                },
                cancellationToken);

            if (!response.ok || response.data == null)
            {
                throw new IGPSDKException(response.error ?? "Authorization refresh failed");
            }

            return response.data;
        }

        private static async Task<IGPAuthorizationOfflineLicenseResponse> RequestOfflineLicensePipeAsync(
            string pipeName,
            string brokerToken,
            string sessionTokenValue,
            string clientVersion,
            CancellationToken cancellationToken)
        {
            var response = await SendAuthorizationPipeRequestAsync<IGPAuthorizationOfflineLicenseEnvelope>(
                pipeName,
                new IGPAuthorizationPipeRequest
                {
                    type = "requestOfflineLicense",
                    authToken = brokerToken,
                    sessionToken = sessionTokenValue,
                    clientVersion = clientVersion,
                },
                cancellationToken);

            if (!response.ok || response.data == null)
            {
                throw new IGPSDKException(response.error ?? "Offline license request failed");
            }

            return response.data;
        }

        private static async Task<TEnvelope> SendAuthorizationPipeRequestAsync<TEnvelope>(
            string pipeEndpoint,
            IGPAuthorizationPipeRequest request,
            CancellationToken cancellationToken)
        {
            var pipeName = ExtractPipeName(pipeEndpoint);
            using var pipe = new NamedPipeClientStream(
                ".",
                pipeName,
                PipeDirection.InOut,
                PipeOptions.Asynchronous);

            await Task.Run(() => pipe.Connect(5000), cancellationToken);
            cancellationToken.ThrowIfCancellationRequested();

            var requestJson = JsonConvert.SerializeObject(request);
            var requestBytes = Encoding.UTF8.GetBytes(requestJson);
            await pipe.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken);
            await pipe.FlushAsync(cancellationToken);

            using var memory = new MemoryStream();
            var buffer = new byte[4096];
            while (true)
            {
                var read = await pipe.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
                if (read <= 0)
                {
                    break;
                }

                memory.Write(buffer, 0, read);
                if (memory.Length > MaxAuthorizationPipeResponseBytes)
                {
                    throw new IGPSDKException("Authorization pipe response is too large");
                }
            }

            var responseJson = Encoding.UTF8.GetString(memory.ToArray());
            var envelope = JsonConvert.DeserializeObject<TEnvelope>(responseJson);
            if (envelope == null)
            {
                throw new IGPSDKException("Authorization pipe returned an empty response");
            }

            return envelope;
        }

        private static string ExtractPipeName(string endpoint)
        {
            const string pipePrefix = @"\\.\pipe\";
            if (endpoint.StartsWith(pipePrefix, StringComparison.OrdinalIgnoreCase))
            {
                return endpoint.Substring(pipePrefix.Length);
            }

            return endpoint;
        }

        private static void VerifyOfflineLicense(
            string token,
            string publicKeyPem,
            string expectedKeyId,
            int expectedGameId,
            string expectedDeviceIdHash)
        {
            if (string.IsNullOrWhiteSpace(token))
            {
                throw new IGPSDKException("Offline license token is missing");
            }

            if (string.IsNullOrWhiteSpace(publicKeyPem))
            {
                throw new IGPSDKException("Offline license public key is missing");
            }

            var parts = token.Split('.');
            if (parts.Length != 3)
            {
                throw new IGPSDKException("Invalid offline license token format");
            }

            var header = JsonConvert.DeserializeObject<IGPAuthorizationJwtHeader>(
                Encoding.UTF8.GetString(DecodeBase64Url(parts[0])));
            var payload = JsonConvert.DeserializeObject<IGPAuthorizationJwtPayload>(
                Encoding.UTF8.GetString(DecodeBase64Url(parts[1])));

            if (header == null || string.IsNullOrWhiteSpace(header.alg))
            {
                throw new IGPSDKException("Offline license token is missing alg");
            }

            if (!string.IsNullOrWhiteSpace(expectedKeyId) &&
                !string.Equals(header.kid, expectedKeyId, StringComparison.Ordinal))
            {
                throw new IGPSDKException("Offline license key id mismatch");
            }

            if (payload == null || !string.Equals(payload.type, "game-offline-license", StringComparison.Ordinal))
            {
                throw new IGPSDKException("Offline license token type mismatch");
            }

            if (payload.gameId != expectedGameId)
            {
                throw new IGPSDKException("Offline license game id mismatch");
            }

            if (!string.Equals(payload.deviceIdHash, expectedDeviceIdHash, StringComparison.Ordinal))
            {
                throw new IGPSDKException("Offline license device mismatch");
            }

            using var rsa = RSA.Create();
            ImportOfflineLicensePublicKey(rsa, publicKeyPem);
            var verified = rsa.VerifyData(
                Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]),
                DecodeBase64Url(parts[2]),
                MapJwtAlgorithm(header.alg),
                RSASignaturePadding.Pkcs1);

            if (!verified)
            {
                throw new IGPSDKException("Offline license signature verification failed");
            }

            var nowUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            if (payload.exp > 0 && payload.exp <= nowUnix)
            {
                throw new IGPSDKException("Offline license expired");
            }
        }

        private static void ImportOfflineLicensePublicKey(RSA rsa, string publicKeyPem)
        {
            if (rsa == null)
            {
                throw new ArgumentNullException(nameof(rsa));
            }

            if (TryImportPemPublicKey(rsa, publicKeyPem, "PUBLIC KEY", useSubjectPublicKeyInfo: true))
            {
                return;
            }

            if (TryImportPemPublicKey(rsa, publicKeyPem, "RSA PUBLIC KEY", useSubjectPublicKeyInfo: false))
            {
                return;
            }

            throw new IGPSDKException("Offline license public key format is invalid");
        }

        private static bool TryImportPemPublicKey(
            RSA rsa,
            string publicKeyPem,
            string label,
            bool useSubjectPublicKeyInfo)
        {
            if (!TryExtractPemBlock(publicKeyPem, label, out var keyBytes))
            {
                return false;
            }

            try
            {
                if (useSubjectPublicKeyInfo)
                {
                    rsa.ImportSubjectPublicKeyInfo(keyBytes, out _);
                }
                else
                {
                    rsa.ImportRSAPublicKey(keyBytes, out _);
                }

                return true;
            }
            catch (CryptographicException)
            {
                return false;
            }
        }

        private static bool TryExtractPemBlock(string pem, string label, out byte[] keyBytes)
        {
            keyBytes = Array.Empty<byte>();
            if (string.IsNullOrWhiteSpace(pem) || string.IsNullOrWhiteSpace(label))
            {
                return false;
            }

            var beginMarker = "-----BEGIN " + label + "-----";
            var endMarker = "-----END " + label + "-----";
            var beginIndex = pem.IndexOf(beginMarker, StringComparison.Ordinal);
            if (beginIndex < 0)
            {
                return false;
            }

            beginIndex += beginMarker.Length;
            var endIndex = pem.IndexOf(endMarker, beginIndex, StringComparison.Ordinal);
            if (endIndex < 0)
            {
                return false;
            }

            var base64Body = pem.Substring(beginIndex, endIndex - beginIndex);
            var normalized = new StringBuilder(base64Body.Length);
            for (var index = 0; index < base64Body.Length; index++)
            {
                var ch = base64Body[index];
                if (!char.IsWhiteSpace(ch))
                {
                    normalized.Append(ch);
                }
            }

            if (normalized.Length == 0)
            {
                return false;
            }

            try
            {
                keyBytes = Convert.FromBase64String(normalized.ToString());
                return keyBytes.Length > 0;
            }
            catch (FormatException)
            {
                keyBytes = Array.Empty<byte>();
                return false;
            }
        }

        private static HashAlgorithmName MapJwtAlgorithm(string algorithm)
        {
            switch (algorithm)
            {
                case "RS256":
                    return HashAlgorithmName.SHA256;
                case "RS384":
                    return HashAlgorithmName.SHA384;
                case "RS512":
                    return HashAlgorithmName.SHA512;
                default:
                    throw new IGPSDKException($"Unsupported offline license algorithm: {algorithm}");
            }
        }

        private static byte[] DecodeBase64Url(string value)
        {
            var normalized = value.Replace('-', '+').Replace('_', '/');
            var padding = 4 - (normalized.Length % 4);
            if (padding < 4)
            {
                normalized += new string('=', padding);
            }

            return Convert.FromBase64String(normalized);
        }

        /// <summary>
        /// 解析启动参数并自动兑换启动票据。
        /// </summary>
        public async Task<bool> TryBootstrapFromLaunchTicketAsync(string[]? launchArgs = null)
        {
            if (!initializeRequested)
            {
                Debug.Log("[IGP] Launch ticket bootstrap skipped because IGP SDK is not initialized.");
                return false;
            }

            IGPLaunchOptions? launchOptions = pendingLaunchOptions;
            if (launchArgs != null)
            {
                if (!IGPLaunchArgsParser.TryParse(launchArgs, out launchOptions))
                {
                    return false;
                }
            }
            else if (launchOptions == null && !IGPLaunchArgsParser.TryParse(Environment.GetCommandLineArgs(), out launchOptions))
            {
                return false;
            }

            if (launchOptions == null || string.IsNullOrEmpty(launchOptions.launchTicket))
            {
                return false;
            }

            return await TryBootstrapFromLaunchOptionsAsync(launchOptions);
        }

        /// <summary>
        /// 从已附着的 desktop session 补领 hosted bootstrap，并连接 hosted session。
        /// </summary>
        public async Task<bool> TryRequestHostedBootstrapFromDesktopSessionAsync(int? appIdOverride = null)
        {
            if (!initializeRequested)
            {
                Debug.Log("[IGP] Hosted bootstrap request skipped because IGP SDK is not initialized.");
                return false;
            }

            if (IsHostedSessionAttached)
            {
                return true;
            }

            if (hostedBootstrapRequestInFlight)
            {
                return false;
            }

            hostedBootstrapRequestInFlight = true;
            try
            {
                var initializationTask = sdkInitializationTask;
                if (!sdkInitialized && initializationTask != null && !initializationTask.IsCompleted)
                {
                    var initialized = await initializationTask;
                    if (!initialized)
                    {
                        return false;
                    }

                    if (IsHostedSessionAttached)
                    {
                        return true;
                    }
                }

                var appId = ResolveDesktopAppId(appIdOverride) ??
                    (desktopSessionClient?.CurrentSession?.AppId > 0
                        ? desktopSessionClient.CurrentSession.AppId
                        : null);
                if (!appId.HasValue)
                {
                    Debug.LogWarning("[IGP] Hosted bootstrap request skipped because appId is missing.");
                    return false;
                }

                var session = await EnsureDesktopSessionAsync(appId.Value);
                EnsureDesktopCapability(
                    currentDesktopCapabilities?.hostedBootstrap ?? false,
                    "Hosted bootstrap is not available on the current desktop session");

                var bootstrap = await session.RequestHostedBootstrapAsync(appId.Value, RuntimeCancellationToken);
                var launchOptions = BuildLaunchOptionsFromHostedBootstrap(bootstrap);
                return await TryBootstrapFromLaunchOptionsAsync(launchOptions);
            }
            catch (Exception ex)
            {
                Debug.LogError($"[IGP] Failed to request hosted bootstrap from desktop session: {ex.Message}");
                onError?.Invoke($"Hosted bootstrap request failed: {ex.Message}");
                return false;
            }
            finally
            {
                hostedBootstrapRequestInFlight = false;
            }
        }

        private async Task<bool> TryBootstrapFromLaunchOptionsAsync(IGPLaunchOptions launchOptions)
        {
            if (!HasHostedLaunchContext(launchOptions))
            {
                return false;
            }

            try
            {
                var bridgeClient = new IGPHostBridgeClient(
                    launchOptions.bootstrapEndpoint,
                    launchOptions.bootstrapSecret);
                var redeem = await bridgeClient.RedeemLaunchTicketAsync(launchOptions.launchTicket);

                if (!string.IsNullOrEmpty(redeem.serverUrl) &&
                    !string.Equals(ServerUrl, redeem.serverUrl, StringComparison.OrdinalIgnoreCase))
                {
                    ServerUrl = redeem.serverUrl;
                    InitializeSDKClients();
                }

                currentRoomId = redeem.roomId;
                currentRoomCode = redeem.roomCode;
                sessionToken = redeem.token;
                playerId = redeem.playerId;
                if (redeem.player != null && !string.IsNullOrEmpty(redeem.player.name))
                {
                    playerName = redeem.player.name;
                }

                if (string.IsNullOrEmpty(redeem.sessionEndpoint) || string.IsNullOrEmpty(redeem.sessionSecret))
                {
                    throw new IGPSDKException("Incomplete hosted bootstrap response");
                }

                ApplyNegotiatedTransportOptions(
                    redeem.reliableMessageMaxBytes,
                    redeem.reliableChunkMaxBytes,
                    redeem.kcpDataPlanePayloadMaxBytes,
                    redeem.kcpFrameMaxBytes);

                await AttachHostedSessionAsync(redeem.sessionEndpoint, redeem.sessionSecret);

                if (launchOptions.autoConnect)
                {
                    await ConnectHostedRealtimeAsync(currentRoomId, currentRoomCode);
                }

                pendingLaunchOptions = null;
                return true;
            }
            catch (Exception ex)
            {
                Debug.LogError($"[IGP] Failed to bootstrap from launch ticket: {ex.Message}");
                onError?.Invoke($"Launch ticket bootstrap failed: {ex.Message}");
                return false;
            }
        }

        private static IGPLaunchOptions BuildLaunchOptionsFromHostedBootstrap(
            IGPDesktopHostedBootstrapContext bootstrap)
        {
            return new IGPLaunchOptions
            {
                launchTicket = bootstrap.launchTicket,
                bootstrapEndpoint = bootstrap.bootstrapEndpoint,
                bootstrapSecret = bootstrap.bootstrapSecret,
                autoConnect = bootstrap.autoConnect,
                appId = bootstrap.appId > 0 ? bootstrap.appId : null,
            };
        }

        private async Task AttachHostedSessionAsync(string sessionEndpoint, string sessionSecret)
        {
            EnsureSDKInitialized();

            await DetachHostedSessionAsync(clearConnectionInfo: false);

            if (string.IsNullOrWhiteSpace(currentRoomId))
            {
                throw new IGPSDKException("Cannot attach hosted session without room context");
            }

            if (string.IsNullOrWhiteSpace(playerId))
            {
                throw new IGPSDKException("Cannot attach hosted session without player context");
            }

            var sessionClient = new IGPHostSessionClient(sessionEndpoint, sessionSecret);
            sessionClient.RoomSnapshotReceived += HandleHostedRoomSnapshot;
            sessionClient.RoomEventReceived += HandleHostedRoomEvent;
            sessionClient.Detached += HandleHostedSessionDetached;
            sessionClient.ErrorOccurred += HandleHostedSessionError;

            try
            {
                await sessionClient.ConnectAsync(currentRoomId, playerId, RuntimeCancellationToken);
                hostSessionClient = sessionClient;
                currentHostedSessionEndpoint = sessionEndpoint;
                currentHostedSessionSecret = sessionSecret;
                WebSocketClient?.AttachHostedSession(sessionClient, currentRoomId, playerId);
                WebSocketClient?.UpdateHostedConnectionState(string.Equals(
                    hostedConnectionStatus,
                    "connected",
                    StringComparison.OrdinalIgnoreCase));
            }
            catch
            {
                sessionClient.RoomSnapshotReceived -= HandleHostedRoomSnapshot;
                sessionClient.RoomEventReceived -= HandleHostedRoomEvent;
                sessionClient.Detached -= HandleHostedSessionDetached;
                sessionClient.ErrorOccurred -= HandleHostedSessionError;
                sessionClient.Dispose();
                throw;
            }
        }

        private async Task DetachHostedSessionAsync(bool clearConnectionInfo = true)
        {
            if (hostSessionClient == null)
            {
                if (clearConnectionInfo)
                {
                    currentHostedSessionEndpoint = string.Empty;
                    currentHostedSessionSecret = string.Empty;
                }
                return;
            }

            var sessionClient = hostSessionClient;
            hostSessionClient = null;
            WebSocketClient?.ClearHostedSession();

            sessionClient.RoomSnapshotReceived -= HandleHostedRoomSnapshot;
            sessionClient.RoomEventReceived -= HandleHostedRoomEvent;
            sessionClient.Detached -= HandleHostedSessionDetached;
            sessionClient.ErrorOccurred -= HandleHostedSessionError;
            await sessionClient.DisconnectAsync();
            if (clearConnectionInfo)
            {
                currentHostedSessionEndpoint = string.Empty;
                currentHostedSessionSecret = string.Empty;
            }
        }

        private IGPHostSessionClient EnsureHostedSession()
        {
            EnsureSDKInitialized();

            if (hostSessionClient == null || !hostSessionClient.IsAttached)
            {
                throw new IGPSDKException(
                    "Room context is required before hosted APIs can be used",
                    IGPErrorCodes.ERR_ROOM_CONTEXT_REQUIRED);
            }

            return hostSessionClient;
        }

        private T ParseHostedCommandPayload<T>(
            IGPHostSessionCommandResult result,
            Func<T> fallbackFactory)
        {
            if (!string.IsNullOrWhiteSpace(result.ContentJson))
            {
                try
                {
                    var parsed = JsonConvert.DeserializeObject<T>(result.ContentJson);
                    if (parsed != null)
                    {
                        return parsed;
                    }
                }
                catch (Exception ex)
                {
                    throw new IGPSDKException($"Failed to parse hosted command payload: {ex.Message}", ex);
                }
            }

            return fallbackFactory();
        }

        private T ParseDesktopCommandPayload<T>(
            IGPDesktopSessionCommandResult result,
            Func<T> fallbackFactory)
        {
            if (!string.IsNullOrWhiteSpace(result.ContentJson))
            {
                try
                {
                    var parsed = JsonConvert.DeserializeObject<T>(result.ContentJson);
                    if (parsed != null)
                    {
                        return parsed;
                    }
                }
                catch (Exception ex)
                {
                    throw new IGPSDKException($"Failed to parse desktop command payload: {ex.Message}", ex);
                }
            }

            return fallbackFactory();
        }

        private void HandleHostedRoomSnapshot(IGPHostSessionSnapshotEvent snapshot)
        {
            if (snapshot.Room == null || snapshot.CurrentPlayer == null)
            {
                return;
            }

            hostedDisconnectRequested = false;
            var hadRoom = roomData != null && !string.IsNullOrEmpty(roomData.id);
            var previousRoomId = roomData?.id ?? string.Empty;
            var previousStatus = roomData?.status ?? string.Empty;
            var previousMapPublicId = roomData?.mapPublicId ?? string.Empty;
            var previousMapVersionId = roomData?.mapVersionId;

            hostedConnectionStatus = snapshot.ConnectionStatus;
            roomData = snapshot.Room;
            currentPlayerData = snapshot.CurrentPlayer;
            currentRoomId = snapshot.Room.id;
            currentRoomCode = snapshot.Room.code;
            playerId = snapshot.CurrentPlayer.id;
            if (!string.IsNullOrEmpty(snapshot.CurrentPlayer.name))
            {
                playerName = snapshot.CurrentPlayer.name;
            }
            WebSocketClient?.UpdateHostedContext(currentRoomId, playerId);
            WebSocketClient?.UpdateHostedConnectionState(string.Equals(
                hostedConnectionStatus,
                "connected",
                StringComparison.OrdinalIgnoreCase));

            if (!hadRoom || !string.Equals(previousRoomId, snapshot.Room.id, StringComparison.Ordinal))
            {
                onRoomJoined?.Invoke(snapshot.Room);
            }
            else
            {
                onRoomUpdated?.Invoke(snapshot.Room);
            }

            if (hadRoom &&
                string.Equals(previousRoomId, snapshot.Room.id, StringComparison.Ordinal) &&
                HasMapChanged(previousMapPublicId, previousMapVersionId, snapshot.Room))
            {
                onMapChanged?.Invoke(new IGPMapChangeData
                {
                    roomId = snapshot.Room.id,
                    previousMapPublicId = previousMapPublicId,
                    previousMapVersionId = previousMapVersionId,
                    currentMapPublicId = snapshot.Room.mapPublicId,
                    currentMapVersionId = snapshot.Room.mapVersionId,
                    room = snapshot.Room,
                });
            }

            if (!string.Equals(previousStatus, snapshot.Room.status, StringComparison.Ordinal) &&
                string.Equals(snapshot.Room.status, "playing", StringComparison.OrdinalIgnoreCase))
            {
                Debug.Log("[IGP] Hosted room status changed to playing");
            }
            else if (!string.Equals(previousStatus, snapshot.Room.status, StringComparison.Ordinal) &&
                string.Equals(snapshot.Room.status, "finished", StringComparison.OrdinalIgnoreCase))
            {
                Debug.Log("[IGP] Hosted room status changed to finished");
            }
        }

        private static bool HasMapChanged(string previousMapPublicId, int? previousMapVersionId, Room currentRoom)
        {
            return !string.Equals(previousMapPublicId, currentRoom.mapPublicId, StringComparison.Ordinal) ||
                   previousMapVersionId != currentRoom.mapVersionId;
        }

        private void HandleHostedRoomEvent(IGPHostSessionRoomEvent roomEvent)
        {
            if (roomEvent == null)
            {
                return;
            }

            if (roomEvent.EventType == IGPHostSessionRoomEventType.HostTransferred)
            {
                if (roomData != null && !string.IsNullOrEmpty(roomEvent.CurrentHostId))
                {
                    roomData.hostId = roomEvent.CurrentHostId;
                }

                onHostTransferred?.Invoke(roomEvent.PreviousHostId, roomEvent.CurrentHostId);
            }

            HostedRoomEventReceived?.Invoke(roomEvent);
        }

        private void HandleDesktopSessionDetached(string reason)
        {
            currentAntiAddictionStatus = null;
            MarkDesktopSessionUnavailable(
                IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED,
                string.IsNullOrWhiteSpace(reason) ? "Desktop session detached" : reason,
                IGPErrorCodes.CATEGORY_CHANNEL,
                channelState: "disconnected",
                attachState: "idle");

            // desktop 掉线通常是通道问题，对齐 GameMaker 的 TryRecoverDesktopAttach：排程下一次重试。
            ScheduleDesktopAttachRetryIfNeeded();
            // desktop 掉了之后授权当然也需要重走一次，排程一次恢复。
            ScheduleAuthorizationRecoverIfNeeded();

            if (!isDestroyed)
            {
                Debug.LogWarning($"[IGP] Desktop session detached: {desktopSessionLastErrorMessage}");
            }
        }

        private void HandleDesktopAntiAddictionChanged(IGPAntiAddictionStatus status)
        {
            currentAntiAddictionStatus = status;
            if (!isDestroyed && status != null)
            {
                onAntiAddictionStateChanged?.Invoke(status);
            }
        }

        private void HandleDesktopHostedBootstrapAvailable(
            IGPDesktopHostedBootstrapAvailableEvent hostedBootstrapAvailable)
        {
            if (isDestroyed || hostedBootstrapAvailable == null || IsHostedSessionAttached)
            {
                return;
            }

            var attachedAppId = desktopSessionClient?.CurrentSession?.AppId;
            if (hostedBootstrapAvailable.appId > 0 &&
                attachedAppId.HasValue &&
                attachedAppId.Value > 0 &&
                hostedBootstrapAvailable.appId != attachedAppId.Value)
            {
                return;
            }

            _ = RunDesktopHostedBootstrapRequestAsync(hostedBootstrapAvailable.appId > 0
                ? hostedBootstrapAvailable.appId
                : attachedAppId);
        }

        private async Task RunDesktopHostedBootstrapRequestAsync(int? appId)
        {
            await TryRequestHostedBootstrapFromDesktopSessionAsync(appId);
        }

        private void HandleDesktopSessionError(string code, string message)
        {
            // 兼容旧事件：只有 code + message 时按码推断分类。
            HandleDesktopSessionErrorCore(
                string.IsNullOrWhiteSpace(code) ? IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED : code,
                message,
                IGPErrorCodes.ResolveCategory(code),
                channelState: null,
                attachState: null);
        }

        private void HandleDesktopSessionErrorDetailed(IGPSDKException error)
        {
            if (error == null)
            {
                return;
            }

            HandleDesktopSessionErrorCore(
                string.IsNullOrWhiteSpace(error.ErrorCode)
                    ? IGPErrorCodes.ERR_DESKTOP_SESSION_REQUIRED
                    : error.ErrorCode!,
                error.Message,
                error.Category,
                error.DesktopChannelState,
                error.DesktopAttachState);
        }

        private void HandleDesktopSessionErrorCore(
            string code,
            string message,
            string? category,
            string? channelState,
            string? attachState)
        {
            MarkDesktopSessionUnavailable(code, message, category, channelState, attachState);

            // 按分类驱动不同的自愈：
            //  - channel：安排 desktop attach retry。
            //  - authorization：安排一次授权状态恢复。
            //  - validation：不重试，交给游戏侧修正配置。
            //  - runtime：同时排程 desktop attach 和授权恢复作为兜底。
            var resolvedCategory = string.IsNullOrWhiteSpace(category)
                ? IGPErrorCodes.ResolveCategory(code)
                : category!;

            if (resolvedCategory == IGPErrorCodes.CATEGORY_AUTHORIZATION)
            {
                ScheduleAuthorizationRecoverIfNeeded();
            }
            else if (resolvedCategory == IGPErrorCodes.CATEGORY_CHANNEL ||
                     resolvedCategory == IGPErrorCodes.CATEGORY_RUNTIME)
            {
                ScheduleDesktopAttachRetryIfNeeded();
                ScheduleAuthorizationRecoverIfNeeded();
            }

            if (!isDestroyed)
            {
                Debug.LogError($"[IGP] Desktop session error ({desktopSessionLastErrorCode}/{resolvedCategory}): {message}");
                onError?.Invoke($"Desktop session error: {message}");
            }
        }

        private void HandleHostedSessionDetached(string reason)
        {
            hostedConnectionStatus = "disconnected";
            if (!isDestroyed)
            {
                Debug.LogWarning($"[IGP] Hosted session detached: {reason}");
                if (ShouldAttemptHostedRecovery())
                {
                    BeginHostedRecovery(reason, restoreDataPlane: ShouldRestoreHostedDataPlane(), requireRealtimeRecovery: true);
                }
                else if (!string.IsNullOrEmpty(currentRoomId))
                {
                    _ = DisconnectAsync();
                }
            }
        }

        private void HandleHostedSessionError(string code, string message)
        {
            if (!isDestroyed)
            {
                Debug.LogError($"[IGP] Hosted session error ({code}): {message}");
                onError?.Invoke($"Hosted session error: {message}");
            }
        }
        
        /// <summary>
        /// 异步断开连接
        /// </summary>
        public async Task DisconnectAsync()
        {
            try
            {
                hostedDisconnectRequested = true;
                StopAuthorizationRefreshLoop();
                CancelHostedRecovery();
                Debug.Log("[IGP] Disconnecting...");
                var previousRoom = roomData;

                // 断开WebSocket
                if (WebSocketClient != null)
                {
                    await WebSocketClient.DisconnectAsync();
                }

                // 断开数据面
                ResetHostedDataPlaneState();

                await DetachHostedSessionAsync();

                // Reset Network P2P sessions
                Network?.ResetSessionState();
                currentRoomId = string.Empty;
                currentRoomCode = string.Empty;
                sessionToken = string.Empty;
                roomData = new Room();
                currentPlayerData = new Player();
                hostedConnectionStatus = "disconnected";

                // 只有在对象未被销毁时才触发事件
                if (!isDestroyed)
                {
                    onConnectionStateChanged?.Invoke(false);
                    if (previousRoom != null)
                    {
                        onRoomLeft?.Invoke(previousRoom);
                    }
                }

                Debug.Log("[IGP] Disconnected successfully");
            }
            catch (Exception ex)
            {
                Debug.LogError($"[IGP] Error during disconnect: {ex.Message}");
                if (!isDestroyed)
                {
                    onError?.Invoke($"Disconnect error: {ex.Message}");
                }
            }
        }

        /// <summary>
        /// 断开连接（同步版本，用于Unity事件或Inspector调用）
        /// </summary>
        public void Disconnect()
        {
            _ = DisconnectAsync().ContinueWith(t =>
            {
                if (t.Exception != null)
                {
                    Debug.LogError($"[IGP] Disconnect failed: {t.Exception.InnerException?.Message}");
                }
            }, TaskScheduler.FromCurrentSynchronizationContext());
        }

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

            Debug.Log(
                $"[IGP] EnsureHostedDataPlaneAttachedAsync: mode={currentDataPlaneMode} " +
                $"transportAttached={dataPlaneTransport != null} hostedSession={IsHostedSessionAttached} " +
                $"realtimeReady={WebSocketClient?.IsConnected ?? false} roomId={currentRoomId}");

            if (currentDataPlaneMode != IGPDataPlaneMode.None && dataPlaneTransport != null)
            {
                Debug.Log("[IGP] Hosted data plane is 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);
        }
        
        #endregion
        
        #region Internal Communication Methods

        /// <summary>
        /// Internal helper to send P2P messages using the best available transport.
        /// </summary>
        internal void SendP2PMessage(Message msg)
        {
            if (IsKcpConnected && dataPlaneTransport != null)
            {
                dataPlaneTransport.SendMessage(msg);
            }
            else
            {
                throw MarkHostedDataPlaneInterrupted(
                    string.IsNullOrWhiteSpace(currentHostedDataPlaneError)
                        ? "Connection interrupted: direct data connection is unavailable"
                        : currentHostedDataPlaneError);
            }
        }

        #endregion

        #region Private Methods

        private IGPSDKException MarkHostedDataPlaneInterrupted(string errorMessage)
        {
            ResetHostedDataPlaneState("interrupted", errorMessage);

            if (!isDestroyed)
            {
                Debug.LogError($"[IGP] {errorMessage}");
                onError?.Invoke(errorMessage);
            }

            return new IGPSDKException(errorMessage);
        }

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

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

            if (suppressKcpRecovery)
            {
                return;
            }

            if (ShouldAttemptHostedDataPlaneRecovery())
            {
                BeginHostedRecovery(
                    "KCP connection dropped",
                    restoreDataPlane: true,
                    requireRealtimeRecovery: false);
            }
        }

        private bool ShouldAttemptHostedRecovery()
        {
            return !hostedDisconnectRequested &&
                   !isDestroyed &&
                   !string.IsNullOrWhiteSpace(currentRoomId) &&
                   !string.IsNullOrWhiteSpace(playerId) &&
                   !string.IsNullOrWhiteSpace(currentHostedSessionEndpoint) &&
                   !string.IsNullOrWhiteSpace(currentHostedSessionSecret);
        }

        private bool ShouldAttemptHostedDataPlaneRecovery()
        {
            return ShouldAttemptHostedRecovery() &&
                   IsHostedSessionAttached &&
                   (WebSocketClient?.IsConnected ?? false) &&
                   ShouldRestoreHostedDataPlane();
        }

        private bool ShouldRestoreHostedDataPlane()
        {
            return hostedRecoveryRestoreDataPlane ||
                   currentDataPlaneMode != IGPDataPlaneMode.None ||
                   dataPlaneTransport != null ||
                   !string.IsNullOrWhiteSpace(currentHostedDataPlaneHost) ||
                   currentHostedDataPlanePort != 0;
        }

        private void BeginHostedRecovery(string reason, bool restoreDataPlane, bool requireRealtimeRecovery)
        {
            if (!ShouldAttemptHostedRecovery())
            {
                return;
            }

            if (hostedRecoveryTask != null && !hostedRecoveryTask.IsCompleted)
            {
                hostedRecoveryRestoreDataPlane |= restoreDataPlane;
                hostedRecoveryRequiresRealtime |= requireRealtimeRecovery;
                return;
            }

            CancelHostedRecovery();
            hostedRecoveryRestoreDataPlane = restoreDataPlane;
            hostedRecoveryRequiresRealtime = requireRealtimeRecovery;
            hostedRecoveryCts = CancellationTokenSource.CreateLinkedTokenSource(RuntimeCancellationToken);
            var cancellationToken = hostedRecoveryCts.Token;
            hostedRecoveryTask = RunHostedRecoveryAsync(reason, cancellationToken);
        }

        private async Task RunHostedRecoveryAsync(string reason, CancellationToken cancellationToken)
        {
            var deadline = DateTime.UtcNow.Add(HostedRecoveryWindow);
            var delay = HostedRecoveryInitialDelay;
            Exception? lastError = null;

            try
            {
                if (hostedRecoveryRestoreDataPlane)
                {
                    ResetHostedDataPlaneState("interrupted", reason);
                }

                hostedConnectionStatus = "recovering";
                currentHostedDataPlaneError = string.Empty;

                while (DateTime.UtcNow < deadline && !cancellationToken.IsCancellationRequested)
                {
                    try
                    {
                        if (hostedRecoveryRequiresRealtime)
                        {
                            await AttachHostedSessionAsync(currentHostedSessionEndpoint, currentHostedSessionSecret);
                            await WaitForHostedRealtimeConnectionAsync();
                            hostedRecoveryRequiresRealtime = false;
                        }
                        else if (!IsHostedSessionAttached || !(WebSocketClient?.IsConnected ?? false))
                        {
                            hostedRecoveryRequiresRealtime = true;
                            continue;
                        }

                        if (hostedRecoveryRestoreDataPlane)
                        {
                            await RequestAndAttachHostedDataPlaneAsync(currentRoomId);
                        }

                        hostedConnectionStatus = "connected";
                        currentHostedDataPlaneError = string.Empty;
                        hostedRecoveryRestoreDataPlane = false;
                        hostedRecoveryRequiresRealtime = true;
                        return;
                    }
                    catch (OperationCanceledException)
                    {
                        return;
                    }
                    catch (Exception ex)
                    {
                        lastError = ex;
                        Debug.LogWarning($"[IGP] Hosted recovery attempt failed: {ex.Message}");
                    }

                    var remaining = deadline - DateTime.UtcNow;
                    if (remaining <= TimeSpan.Zero)
                    {
                        break;
                    }

                    var waitDelay = delay < remaining ? delay : remaining;
                    try
                    {
                        await Task.Delay(waitDelay, cancellationToken);
                    }
                    catch (OperationCanceledException)
                    {
                        return;
                    }
                    delay = delay + delay < HostedRecoveryMaxDelay ? delay + delay : HostedRecoveryMaxDelay;
                }

                if (!isDestroyed && !hostedDisconnectRequested)
                {
                    hostedConnectionStatus = "recovery-failed";
                    currentHostedDataPlaneStatus = hostedRecoveryRestoreDataPlane ? "recovery-failed" : currentHostedDataPlaneStatus;
                    if (lastError != null)
                    {
                        currentHostedDataPlaneError = lastError.Message;
                        onError?.Invoke($"Connection recovery failed after {HostedRecoveryWindow.TotalSeconds:F0}s: {lastError.Message}");
                    }
                    else
                    {
                        onError?.Invoke($"Connection recovery failed after {HostedRecoveryWindow.TotalSeconds:F0}s");
                    }
                }
            }
            finally
            {
                hostedRecoveryRequiresRealtime = false;
                hostedRecoveryCts?.Dispose();
                hostedRecoveryCts = null;
            }
        }

        private void CancelHostedRecovery()
        {
            if (hostedRecoveryCts != null)
            {
                hostedRecoveryCts.Cancel();
                hostedRecoveryCts.Dispose();
                hostedRecoveryCts = null;
            }
        }

        /// <summary>
        /// 通过宿主实时控制面发送自定义消息。
        /// </summary>
        public async Task SendMessageAsync(Message message)
        {
            if (WebSocketClient == null)
            {
                throw new IGPSDKException("IGP realtime client is not initialized");
            }

            await WebSocketClient.SendMessageAsync(message);
        }

        /// <summary>
        /// 通过宿主实时控制面发送 ping。
        /// </summary>
        public async Task SendPingAsync()
        {
            if (WebSocketClient == null)
            {
                throw new IGPSDKException("IGP realtime client is not initialized");
            }

            await WebSocketClient.SendPingAsync();
        }

        /// <summary>
        /// 设置指定作用域的状态。
        /// </summary>
        public async Task SetStateAsync(string scope, string key, object? value, string? targetPlayerId = null, bool reliable = true)
        {
            if (WebSocketClient == null)
            {
                throw new IGPSDKException("IGP realtime client is not initialized");
            }

            await WebSocketClient.SetStateAsync(scope, key, value, targetPlayerId);
        }

        /// <summary>
        /// 获取指定作用域的状态。
        /// </summary>
        public async Task GetStateAsync(string scope, string key, string? targetPlayerId = null)
        {
            if (WebSocketClient == null)
            {
                throw new IGPSDKException("IGP realtime client is not initialized");
            }

            await WebSocketClient.GetStateAsync(scope, key, targetPlayerId);
        }

        /// <summary>
        /// 重置指定作用域的状态。
        /// </summary>
        public async Task ResetStateAsync(string scope, string[]? keysToExclude = null)
        {
            if (WebSocketClient == null)
            {
                throw new IGPSDKException("IGP realtime client is not initialized");
            }

            await WebSocketClient.ResetStateAsync(scope, keysToExclude);
        }

        /// <summary>
        /// 注册 RPC。
        /// </summary>
        public async Task RegisterRPCAsync(string name)
        {
            if (WebSocketClient == null)
            {
                throw new IGPSDKException("IGP realtime client is not initialized");
            }

            await WebSocketClient.RegisterRPCAsync(name);
        }

        /// <summary>
        /// 取消注册 RPC。
        /// </summary>
        public async Task UnregisterRPCAsync(string name)
        {
            if (WebSocketClient == null)
            {
                throw new IGPSDKException("IGP realtime client is not initialized");
            }

            await WebSocketClient.UnregisterRPCAsync(name);
        }

        /// <summary>
        /// 通过宿主实时控制面调用 RPC。
        /// </summary>
        public async Task<string> CallRPCAsync(string name, object? data = null, string mode = "all", string? requestId = null)
        {
            if (WebSocketClient == null)
            {
                throw new IGPSDKException("IGP realtime client is not initialized");
            }

            return await WebSocketClient.CallRPCAsync(name, data, mode, requestId);
        }

        /// <summary>
        /// 设置全局状态
        /// </summary>
        /// <param name="key">状态键</param>
        /// <param name="value">状态值</param>
        public async Task SetGlobalStateAsync(string key, object value)
        {
            await SetStateAsync("global", key, value);
        }

        /// <summary>
        /// 设置玩家状态
        /// </summary>
        /// <param name="key">状态键</param>
        /// <param name="value">状态值</param>
        public async Task SetPlayerStateAsync(string key, object value)
        {
            await SetStateAsync("player", key, value);
        }
        
        /// <summary>
        /// 根据游戏状态调整KCP心跳间隔
        /// </summary>
        public void SetHeartbeatIntervalForGameState(GameState state)
        {
            switch (state)
            {
                case GameState.InBattle: kcpHeartbeatInterval = 0.1f; break;
                case GameState.InGame: kcpHeartbeatInterval = 1.0f; break;
                case GameState.Idle: kcpHeartbeatInterval = 5.0f; break;
            }
            
            if (dataPlaneTransport != null)
            {
                dataPlaneTransport.SetHeartbeatInterval(kcpHeartbeatInterval);
            }
            else if (KcpClient != null && enableKcpHeartbeat)
            {
                KcpClient.HeartbeatInterval = kcpHeartbeatInterval;
            }
        }
        
        #endregion

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

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

            var session = EnsureHostedSession();
            Exception? lastError = null;
            for (var attempt = 1; attempt <= HostedDataPlaneAttachAttempts; attempt += 1)
            {
                currentHostedDataPlaneStatus = attempt == 1 ? "requesting" : $"retrying:{attempt}";
                currentHostedDataPlaneError = string.Empty;

                try
                {
                    var dataPlaneResponse = await session.RequestDataPlaneAsync(RuntimeCancellationToken);
                    currentHostedDataPlaneStatus = $"descriptor-received:{attempt}";
                    await AttachHostedDataPlaneAsync(roomId, dataPlaneResponse);
                    await WaitForHostedDataPlaneReadyAsync(HostedDataPlaneReadyTimeout);
                    return;
                }
                catch (OperationCanceledException)
                {
                    const string error = "Hosted data plane request timed out after 10s";
                    currentHostedDataPlaneStatus = "failed:timeout";
                    currentHostedDataPlaneError = error;
                    Debug.LogError($"[IGP] {error}");
                    onError?.Invoke(error);
                    throw;
                }
                catch (Exception ex)
                {
                    lastError = ex;
                    ResetHostedDataPlaneState($"retrying:{attempt}", ex.Message);
                    if (attempt >= HostedDataPlaneAttachAttempts)
                    {
                        currentHostedDataPlaneStatus = "failed";
                        currentHostedDataPlaneError = ex.Message;
                        Debug.LogError($"[IGP] Hosted data plane attach failed: {ex.Message}");
                        throw;
                    }

                    Debug.LogWarning($"[IGP] Hosted data plane attach attempt {attempt} failed: {ex.Message}. Retrying...");
                    await Task.Delay(HostedDataPlaneRetryDelay, RuntimeCancellationToken);
                }
            }

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

        private Task AttachHostedDataPlaneAsync(string roomId, IGPHostSessionDataPlaneResponse dataPlaneResponse)
        {
            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, dataPlaneResponse.DataPlane);
                    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, IGPHostSessionDataPlaneDescriptor descriptor)
        {
            if (KcpClient == null)
            {
                throw new IGPSDKException("IGP KCP client is not initialized");
            }

            dataPlaneTransport?.Disconnect();
            dataPlaneTransport = IGPDataPlaneTransportFactory.Create(descriptor, KcpClient);
            dataPlaneTransport.Connect(descriptor, roomId, PlayerId);
            currentDataPlaneMode = dataPlaneTransport.Mode;
            currentHostedDataPlaneHost = descriptor.Host ?? string.Empty;
            currentHostedDataPlanePort = descriptor.Port;
            currentHostedDataPlaneStatus = "connecting";
            currentHostedDataPlaneError = string.Empty;

            Debug.Log($"[IGP] Hosted data plane target: mode={descriptor.Mode} host={currentHostedDataPlaneHost} port={currentHostedDataPlanePort}");

            if (enableKcpHeartbeat)
            {
                dataPlaneTransport.StartHeartbeat(kcpHeartbeatInterval);
                Debug.Log($"[IGP] KCP heartbeat started (interval: {kcpHeartbeatInterval}s)");
            }
        }

        private async Task WaitForHostedDataPlaneReadyAsync(TimeSpan timeout)
        {
            var deadline = DateTime.UtcNow.Add(timeout);
            while (DateTime.UtcNow < deadline)
            {
                if (IsKcpConnected)
                {
                    currentHostedDataPlaneStatus = "connected";
                    currentHostedDataPlaneError = string.Empty;
                    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 = "")
        {
            suppressKcpRecovery = true;
            try
            {
                dataPlaneTransport?.Disconnect();
            }
            finally
            {
                suppressKcpRecovery = false;
            }
            dataPlaneTransport = null;
            currentDataPlaneMode = IGPDataPlaneMode.None;
            currentHostedDataPlaneHost = string.Empty;
            currentHostedDataPlanePort = 0;
            currentHostedDataPlaneStatus = status;
            currentHostedDataPlaneError = error;
        }

        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);
        }
        
        #region Private Methods
        
        /// <summary>
        /// 生成唯一的玩家ID
        /// </summary>
        private string GeneratePlayerId()
        {
            return $"player_{Guid.NewGuid().ToString("N").Substring(0, 8)}";
        }
        
        /// <summary>
        /// 心跳协程
        /// </summary>
        private System.Collections.IEnumerator HeartbeatCoroutine()
        {
            while (true)
            {
                yield return new WaitForSeconds(HeartbeatInterval);

                if (isDestroyed) yield break; // 对象已销毁，停止协程

                if (IsWebSocketConnected && WebSocketClient != null)
                {
                    try
                    {
                        // 发送心跳
                        _ = WebSocketClient.SendPingAsync();

                        onHeartbeatSent?.Invoke();
                        Debug.Log("[IGP] Heartbeat sent");
                    }
                    catch (Exception ex)
                    {
                        if (!isDestroyed) Debug.LogError($"[IGP] Heartbeat failed: {ex.Message}");
                    }
                }
            }
        }
        
        #endregion
    }

    internal sealed class IGPAuthorizationPipeRequest
    {
        public string type = string.Empty;
        public string authToken = string.Empty;
        public string sessionToken = string.Empty;
        public string clientVersion = string.Empty;
    }

    internal sealed class IGPAuthorizationBootstrapPayload
    {
        public string launchTicket = string.Empty;
        public int gameId;
        public bool autoAuth = true;
        public string deviceIdHash = string.Empty;
        public bool offlineMode;
        public bool authorizationRequired = true;
        public string brokerToken = string.Empty;
        public string offlineLicensePublicKey = string.Empty;
        public string offlineLicenseKeyId = string.Empty;
        public string offlineLicenseAlgorithm = string.Empty;
    }

    internal sealed class IGPAuthorizationPolicy
    {
        public int refreshBeforeSeconds = 30;
        public bool offlineSupported;
    }

    internal sealed class IGPAuthorizationSessionResponse
    {
        public string sessionToken = string.Empty;
        public string expiresAt = string.Empty;
        public IGPAuthorizationPolicy? policy;
    }

    internal sealed class IGPAuthorizationOfflineLicenseResponse
    {
        public string offlineLicense = string.Empty;
        public string expiresAt = string.Empty;
        public IGPAuthorizationPolicy? policy;
    }

    internal sealed class IGPAuthorizationBootstrapEnvelope
    {
        public bool ok;
        public string error = string.Empty;
        public string type = string.Empty;
        public IGPAuthorizationBootstrapPayload? data;
    }

    internal sealed class IGPAuthorizationSessionEnvelope
    {
        public bool ok;
        public string error = string.Empty;
        public string type = string.Empty;
        public IGPAuthorizationSessionResponse? data;
    }

    internal sealed class IGPAuthorizationOfflineLicenseEnvelope
    {
        public bool ok;
        public string error = string.Empty;
        public string type = string.Empty;
        public IGPAuthorizationOfflineLicenseResponse? data;
    }

    internal sealed class IGPAuthorizationJwtHeader
    {
        public string alg = string.Empty;
        public string kid = string.Empty;
        public string typ = string.Empty;
    }

    internal sealed class IGPAuthorizationJwtPayload
    {
        public string sub = string.Empty;
        public int gameId;
        public string lid = string.Empty;
        public string jti = string.Empty;
        public string type = string.Empty;
        public string deviceIdHash = string.Empty;
        public long exp;
    }

    internal sealed class IGPAuthorizationCacheRecord
    {
        public string offlineLicense = string.Empty;
        public string expiresAt = string.Empty;
        public string deviceIdHash = string.Empty;
        public int gameId;
        public string publicKey = string.Empty;
        public string keyId = string.Empty;
        public string algorithm = string.Empty;
        public string clientVersion = string.Empty;
        public string savedAt = string.Empty;
    }

    internal sealed class IGPAuthorizationCacheEnvelope
    {
        public int version = 1;
        public string protection = string.Empty;
        public string payload = string.Empty;
    }
}
