#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
using IGP.UnitySDK.Abstractions;
using IGP.UnitySDK.Models;

namespace IGP.UnitySDK
{
    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>
    /// Core Unity SDK runtime. Multiplayer behavior is supplied by optional
    /// <see cref="IIGPRuntimeExtension"/> components.
    /// </summary>
    public partial class IGPRuntimeManager : MonoBehaviour,
        IIGPRuntimeClient,
        IIGPRuntimeContext,
        IIGPDesktopCommandGateway,
        IIGPDesktopNotificationSource
    {
        private const string PreviousEnglishAuthorizationFailureFallbackTitle = "Network unavailable";
        private const string PreviousEnglishAuthorizationFailureFallbackMessage =
            "Network is currently unavailable. Please check your connection and try again.";
        private const string PreviousEnglishAuthorizationFailureFallbackHint =
            "Please check your network connection and retry authorization.";
        private const string PreviousEnglishAuthorizationFailureFallbackButtonText = "Retry";
        private const string LegacyAuthorizationFailureFallbackTitle = "Unable to verify game access";
        private const string LegacyAuthorizationFailureFallbackHint =
            "Open IGP Desktop, make sure you are signed in, and then try again.";
        private const string LegacyAuthorizationFailureFallbackButtonText = "OK";
        private const int MaxAuthorizationPipeResponseBytes = 1024 * 1024;
        private const double DesktopAttachRetryBackoffSeconds = 2.0;
        private const double AuthorizationRecoverBackoffSeconds = 2.0;

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

        public event Action<IGPConnectionChangedEvent>? ConnectionChanged;
        public event Action<IGPAuthorizationChangedEvent>? AuthorizationChanged;
        public event Action<IGPAntiAddictionStatus>? AntiAddictionChanged;
        public event Action<IGPDesktopOfflineMapLaunchRequestedEvent>? OfflineMapLaunchRequested;
        public event Action? GracefulShutdownRequested;
        public event Action<IGPSDKException>? ErrorOccurred;

        private event Action<IGPDesktopSessionCommandResult>? desktopNotificationReceived;

        private CancellationTokenSource? cancellationTokenSource;
        private readonly List<IIGPRuntimeExtension> initializedExtensions = new List<IIGPRuntimeExtension>();
        private bool isDestroyed;
        private bool initializeRequested;
        private bool sdkInitialized;
        private Task<bool>? sdkInitializationTask;
        private Task? runtimeExtensionsShutdownTask;
        private IGPCoreLaunchContext? pendingLaunchOptions;
        private IGPDesktopSessionClient? desktopSessionClient;
        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 bool desktopConnectionEventState;
        private DateTime? nextAuthorizationRecoverAtUtc;
        private bool authorizationRecoverInFlight;
        private IGPAuthorizationState authorizationState = IGPAuthorizationState.Pending;
        private IGPAuthorizationState? lastPublishedAuthorizationState;
        private bool lastPublishedAuthorizationRequired;
        private string lastPublishedAuthorizationMessage = string.Empty;
        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 IGPLogLevel appliedLogLevel = IGPLogLevel.Off;

        private CancellationToken RuntimeCancellationToken =>
            cancellationTokenSource?.Token ?? CancellationToken.None;

        public CancellationToken LifetimeToken => RuntimeCancellationToken;
        public bool IsInitialized => sdkInitialized;
        public IGPSDKEnvironment SdkEnvironment => config?.sdkEnvironment ?? IGPSDKEnvironment.PROD;
        public IGPLogLevel LogLevel => config?.debugLogging ?? IGPLogLevel.Off;
        public IGPConfig? Config => config;
        public IIGPDesktopCommandGateway Desktop => this;
        event Action<IGPDesktopSessionCommandResult>? IIGPDesktopNotificationSource.NotificationReceived
        {
            add => desktopNotificationReceived += value;
            remove => desktopNotificationReceived -= value;
        }
        public bool IsAttached => IsDesktopSessionAttached;
        public bool IsDesktopSessionAttached => desktopSessionClient?.IsAttached ?? false;
        public IGPDesktopUserContext? CurrentDesktopUserContext => currentDesktopUserContext;
        public IGPDesktopCapabilitySet? CurrentDesktopCapabilities => currentDesktopCapabilities;
        public IGPDesktopUserProfile? CurrentDesktopUserProfile => currentDesktopUserProfile;
        public IGPAntiAddictionStatus? CurrentAntiAddictionStatus => currentAntiAddictionStatus;
        public IGPAuthorizationState AuthorizationState => authorizationState;
        public bool IsAuthorized =>
            authorizationState == IGPAuthorizationState.AuthorizedOnline ||
            authorizationState == IGPAuthorizationState.AuthorizedOffline;
        public bool IsAuthorizationRequired => isAuthorizationRequired;
        public IGPAuthorizationFailureInfo? LastAuthorizationFailure => lastAuthorizationFailure;

        public string DesktopChannelState => desktopSessionClient?.CurrentSession?.ChannelState
            ?? desktopSessionLastChannelState;
        public string DesktopAttachState => desktopSessionClient?.CurrentSession?.AttachState
            ?? desktopSessionLastAttachState;
        public string DesktopAttachSource => desktopSessionClient?.CurrentSession?.AttachSource
            ?? desktopSessionLastAttachSource;
        public string DesktopSessionLastErrorCategory => desktopSessionLastErrorCategory;

        public bool ShowAuthorizationFailureFallback =>
            config == null || config.showAuthorizationFailureFallback;
        public string AuthorizationFailureFallbackTitle =>
            config?.authorizationFailureFallbackTitle ?? IGPConfig.DefaultAuthorizationFailureFallbackTitle;
        public string AuthorizationFailureFallbackMessage =>
            config?.authorizationFailureFallbackMessage ?? IGPConfig.DefaultAuthorizationFailureFallbackMessage;
        public string AuthorizationFailureFallbackHint =>
            config?.authorizationFailureFallbackHint ?? IGPConfig.DefaultAuthorizationFailureFallbackHint;
        public string AuthorizationFailureFallbackButtonText =>
            config?.authorizationFailureFallbackButtonText ?? IGPConfig.DefaultAuthorizationFailureFallbackButtonText;
        public string AuthorizationFailureFallbackQuitButtonText =>
            config?.authorizationFailureFallbackQuitButtonText ?? IGPConfig.DefaultAuthorizationFailureFallbackQuitButtonText;
        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.");
            }
        }

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

            config = ResolveConfigAssetInEditor();
            if (config != null)
            {
                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 > 0)
            {
                Array.Sort(guids, StringComparer.Ordinal);
                return AssetDatabase.LoadAssetAtPath<IGPConfig>(AssetDatabase.GUIDToAssetPath(guids[0]));
            }

            EnsureDefaultConfigFolderInEditor();
            var createdConfig = ScriptableObject.CreateInstance<IGPConfig>();
            AssetDatabase.CreateAsset(createdConfig, DefaultConfigAssetPath);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            return createdConfig;
        }

        private static void EnsureDefaultConfigFolderInEditor()
        {
            if (!AssetDatabase.IsValidFolder("Assets/IGP"))
            {
                AssetDatabase.CreateFolder("Assets", "IGP");
            }
        }
#endif

        private void Awake()
        {
            IGPLog.Configure(LogLevel);
            cancellationTokenSource ??= new CancellationTokenSource();
        }

        public async Task<bool> InitializeAsync()
        {
            if (sdkInitializationTask != null)
            {
                return await sdkInitializationTask;
            }

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

        public void Initialize()
        {
            _ = RunInitializeAndReportAsync();
        }

        private async Task RunInitializeAndReportAsync()
        {
            try
            {
                await InitializeAsync();
            }
            catch (Exception ex)
            {
                LogSdkError("runtime", "initialize", $"event=failed error={FormatNetworkLogValue(ex.Message)}");
                ErrorOccurred?.Invoke(ex as IGPSDKException ??
                    new IGPSDKException($"SDK initialization failed: {ex.Message}", ex));
            }
        }

        private async Task<bool> InitializeInternalAsync()
        {
            try
            {
                cancellationTokenSource ??= new CancellationTokenSource();
                pendingLaunchOptions = ParseCoreLaunchContext(Environment.GetCommandLineArgs());
                ApplyLogSettings();
                LogSessionStart();

                if (ShouldAutoAttachDesktopSessionOnStartup())
                {
                    var attached = await TryAttachDesktopSessionAsync();
                    if (!attached)
                    {
                        await TryLaunchDesktopAndRetryAttachAsync();
                    }
                }

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

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

                sdkInitialized = true;
                LogSdkInfo("runtime", "initialize", "event=succeeded");
                return true;
            }
            catch (Exception ex)
            {
                sdkInitialized = false;
                await ShutdownRuntimeExtensionsAsync();
                LogSdkError("runtime", "initialize", $"event=failed error={FormatNetworkLogValue(ex.Message)}");
                ErrorOccurred?.Invoke(ex as IGPSDKException ??
                    new IGPSDKException($"SDK initialization failed: {ex.Message}", ex));
                return false;
            }
        }

        private async Task<bool> InitializeRuntimeExtensionsAsync()
        {
            var extensions = new List<IIGPRuntimeExtension>();
            foreach (var behaviour in GetComponents<MonoBehaviour>())
            {
                if (behaviour is IIGPRuntimeExtension extension)
                {
                    extensions.Add(extension);
                }
            }

            extensions.Sort((left, right) => left.InitializationOrder.CompareTo(right.InitializationOrder));
            foreach (var extension in extensions)
            {
                if (isDestroyed || RuntimeCancellationToken.IsCancellationRequested)
                {
                    await ShutdownRuntimeExtensionsAsync();
                    return false;
                }

                initializedExtensions.Add(extension);
                if (!await extension.InitializeAsync(this, RuntimeCancellationToken))
                {
                    await ShutdownRuntimeExtensionsAsync();
                    return false;
                }
            }

            return true;
        }

        private Task ShutdownRuntimeExtensionsAsync()
        {
            runtimeExtensionsShutdownTask ??= ShutdownRuntimeExtensionsInternalAsync();
            return runtimeExtensionsShutdownTask;
        }

        private async Task ShutdownRuntimeExtensionsInternalAsync()
        {
            var extensions = initializedExtensions.ToArray();
            initializedExtensions.Clear();
            for (var index = extensions.Length - 1; index >= 0; index--)
            {
                try
                {
                    await extensions[index].ShutdownAsync(CancellationToken.None);
                }
                catch (Exception ex)
                {
                    LogSdkWarning("runtime-extension", "shutdown", $"event=failed error={FormatNetworkLogValue(ex.Message)}");
                }
            }

        }

        private void Update()
        {
            ApplyLogSettingsIfNeeded();
            DrainDesktopGracefulShutdownRequests();
            TryRecoverDesktopAttachIfNeeded();
            TryRecoverAuthorizationStateIfNeeded();
        }

        private void OnGUI()
        {
            RenderAuthorizationFailureFallback();
        }

        private void OnDestroy()
        {
            isDestroyed = true;
            StopAuthorizationRefreshLoop();
            cancellationTokenSource?.Cancel();
            _ = ShutdownRuntimeExtensionsAsync();
            desktopSessionClient?.Dispose();
            cancellationTokenSource?.Dispose();
            cancellationTokenSource = null;
        }

        private void ApplyLogSettings()
        {
            IGPLog.Configure(LogLevel);
            appliedLogLevel = LogLevel;
        }

        private void ApplyLogSettingsIfNeeded()
        {
            if (appliedLogLevel != LogLevel)
            {
                ApplyLogSettings();
            }
        }

        private void LogSessionStart()
        {
            var appId = ResolveDesktopAppId(null);
            LogSdkInfo(
                "session",
                "start",
                $"event=session-start role=unity-client appId={(appId.HasValue ? appId.Value.ToString() : "-")} " +
                $"sdkVersion={FormatNetworkLogValue(ResolveSdkVersion())} transport=desktop-session " +
                $"endpoint={FormatNetworkLogValue(DesktopPipeEndpoint)}");
        }

        private void LogSdkInfo(string path, string message) => LogSdkInfo("core", path, message);
        private void LogSdkWarning(string path, string message) => LogSdkWarning("core", path, message);
        private void LogSdkError(string path, string message) => LogSdkError("core", path, message);
        private void LogSdkInfo(string scope, string path, string message) => IGPLog.Info(scope, path, message);
        private void LogSdkWarning(string scope, string path, string message) => IGPLog.Warning(scope, path, message);
        private void LogSdkError(string scope, string path, string message) => IGPLog.Error(scope, path, message);
        private static string FormatNetworkLogValue(string? value) => IGPLog.FormatValue(value);

        private T ParseDesktopCommandPayload<T>(
            IGPDesktopSessionCommandResult result,
            Func<T> fallbackFactory)
        {
            if (string.IsNullOrWhiteSpace(result.ContentJson))
            {
                return fallbackFactory();
            }

            try
            {
                return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(result.ContentJson)
                    ?? fallbackFactory();
            }
            catch (Exception ex)
            {
                throw new IGPSDKException($"Failed to parse desktop command payload: {ex.Message}", ex);
            }
        }

        private static IGPCoreLaunchContext? ParseCoreLaunchContext(string[] args)
        {
            var context = new IGPCoreLaunchContext();
            for (var index = 0; index < args.Length; index++)
            {
                var argument = args[index];
                if (TryReadLaunchOption(args, ref index, argument, "--igp-ipc-pipe", out var pipe))
                {
                    context.ipcPipeName = pipe;
                }
                else if (TryReadLaunchOption(args, ref index, argument, "--igp-app-id", out var appId) ||
                         TryReadLaunchOption(args, ref index, argument, "--arena-app-id", out appId))
                {
                    if (int.TryParse(appId, out var parsedAppId) && parsedAppId > 0)
                    {
                        context.appId = parsedAppId;
                    }
                }
            }

            return context.appId.HasValue || !string.IsNullOrWhiteSpace(context.ipcPipeName)
                ? context
                : null;
        }

        private static bool TryReadLaunchOption(
            string[] args,
            ref int index,
            string argument,
            string option,
            out string value)
        {
            value = string.Empty;
            if (argument.StartsWith(option + "=", StringComparison.Ordinal))
            {
                value = argument.Substring(option.Length + 1);
                return true;
            }

            if (!string.Equals(argument, option, StringComparison.Ordinal) || index + 1 >= args.Length)
            {
                return false;
            }

            value = args[++index];
            return true;
        }
    }

    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 IGPCoreLaunchContext
    {
        public string ipcPipeName = string.Empty;
        public int? appId;
    }

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