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

namespace IGP.UnitySDK
{
    public partial class IGPRuntimeManager
    {

        private void 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)
            {
                LogSdkWarning("authorization", "recovery", $"event=failed error={FormatNetworkLogValue(ex.Message)}");
                if (!isDestroyed &&
                    authorizationState != IGPAuthorizationState.AuthorizedOnline &&
                    authorizationState != IGPAuthorizationState.Skipped)
                {
                    nextAuthorizationRecoverAtUtc = DateTime.UtcNow.AddSeconds(AuthorizationRecoverBackoffSeconds);
                }
            }
            finally
            {
                authorizationRecoverInFlight = false;
            }
        }


        /// <summary>
        /// 手动关闭当前授权验证失败保底提示。
        /// </summary>
        public void DismissAuthorizationFailureFallback()
        {
            authorizationFailureFallbackDismissed = true;
        }

        /// <summary>
        /// Immediately retries the current authorization validation flow.
        /// </summary>
        public void RetryAuthorization()
        {
            RequestAuthorizationRetryFromFallback();
        }

        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)
            {
                LogSdkWarning("authorization", "offline-cache", $"event=update-failed error={FormatNetworkLogValue(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)
            {
                LogSdkWarning("authorization", "offline-cache", $"event=invalid error={FormatNetworkLogValue(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 + 20f, panelY + panelHeight - 44f, 100f, 24f),
                    GetAuthorizationFailureFallbackQuitButtonText()))
            {
                QuitGameFromAuthorizationFailureFallback();
            }

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

        private void RequestAuthorizationRetryFromFallback()
        {
            if (isDestroyed || authorizationRecoverInFlight)
            {
                return;
            }

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

        private void QuitGameFromAuthorizationFailureFallback()
        {
#if UNITY_EDITOR
            if (Application.isPlaying)
            {
                EditorApplication.isPlaying = false;
                return;
            }
#endif
            Application.Quit();
        }

        private string GetAuthorizationFailureFallbackTitle()
        {
            var configuredTitle = AuthorizationFailureFallbackTitle;
            if (!string.IsNullOrWhiteSpace(configuredTitle) &&
                !string.Equals(
                    configuredTitle,
                    LegacyAuthorizationFailureFallbackTitle,
                    StringComparison.Ordinal) &&
                !string.Equals(
                    configuredTitle,
                    PreviousEnglishAuthorizationFailureFallbackTitle,
                    StringComparison.Ordinal))
            {
                return configuredTitle;
            }

            return IGPConfig.DefaultAuthorizationFailureFallbackTitle;
        }

        private string GetAuthorizationFailureDisplayMessage()
        {
            var configuredMessage = AuthorizationFailureFallbackMessage;
            if (!string.IsNullOrWhiteSpace(configuredMessage) &&
                !string.Equals(
                    configuredMessage,
                    PreviousEnglishAuthorizationFailureFallbackMessage,
                    StringComparison.Ordinal))
            {
                return configuredMessage;
            }

            return IGPConfig.DefaultAuthorizationFailureFallbackMessage;
        }

        private string GetAuthorizationFailureFallbackHint()
        {
            var configuredHint = AuthorizationFailureFallbackHint;
            if (!string.IsNullOrWhiteSpace(configuredHint) &&
                !string.Equals(
                    configuredHint,
                    LegacyAuthorizationFailureFallbackHint,
                    StringComparison.Ordinal) &&
                !string.Equals(
                    configuredHint,
                    PreviousEnglishAuthorizationFailureFallbackHint,
                    StringComparison.Ordinal))
            {
                return configuredHint;
            }

            return IGPConfig.DefaultAuthorizationFailureFallbackHint;
        }

        private string GetAuthorizationFailureFallbackButtonText()
        {
            var configuredButtonText = AuthorizationFailureFallbackButtonText;
            if (!string.IsNullOrWhiteSpace(configuredButtonText) &&
                !string.Equals(
                    configuredButtonText,
                    LegacyAuthorizationFailureFallbackButtonText,
                    StringComparison.Ordinal) &&
                !string.Equals(
                    configuredButtonText,
                    PreviousEnglishAuthorizationFailureFallbackButtonText,
                    StringComparison.Ordinal))
            {
                return configuredButtonText;
            }

            return IGPConfig.DefaultAuthorizationFailureFallbackButtonText;
        }

        private string GetAuthorizationFailureFallbackQuitButtonText()
        {
            var configuredQuitButtonText = AuthorizationFailureFallbackQuitButtonText;
            if (!string.IsNullOrWhiteSpace(configuredQuitButtonText))
            {
                return configuredQuitButtonText;
            }

            return IGPConfig.DefaultAuthorizationFailureFallbackQuitButtonText;
        }

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