#nullable enable
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;

namespace IGP.UnitySDK
{
    public enum IGPSDKEnvironment
    {
        PROD = 0,
        DEV = 1,
    }

    internal static class IGPDesktopSessionEnvironment
    {
        public const string DefaultPipeEndpoint = ProdPipeEndpoint;
        public const string ProdPipeEndpoint = @"\\.\pipe\igp-desktop-sdk";
        public const string DevPipeEndpoint = @"\\.\pipe\indiespark-desktop-sdk";
        public const string DesktopPipeEndpointEnvironmentVariable = "INDIEGP_DESKTOP_PIPE_ENDPOINT";
        public const string DesktopPathEnvironmentVariable = "INDIEGP_DESKTOP_PATH";
        public const string AppIdEnvironmentVariable = "INDIEGP_APP_ID";
        public const string LegacyAppIdEnvironmentVariable = "IGP_APP_ID";

        private const string ProdDesktopExecutableName = "IndieGamesPass.exe";
        private const string DevDesktopExecutableName = "IndieSpark.exe";
        private const string AppPathsRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\App Paths\IndieGamesPass.exe";
        private const string DevAppPathsRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\App Paths\IndieSpark.exe";
        private const string IndieSparkDesktopInstallRegistryPath = @"Software\IndieSpark";
        private const string CurioDesktopInstallRegistryPath = @"Software\IndieGamesPass";
        private const string CurioDesktopInstallRegistryValue = "InstallPath";
        private const string DesktopInstallRegistryPath = @"Software\IndieGP\Desktop";
        private const string DesktopInstallRegistryValue = "InstallPath";
        private static readonly string[] OfficialDesktopCertificateSubjectTokens =
        {
            "Indie Games Pass",
            "IndieGamesPass",
            "IndieGP",
        };
        private static readonly string[] LocalProgramsInstallFolders =
        {
            "IndieGamesPass",
        };
        private static readonly string[] DevLocalProgramsInstallFolders =
        {
            "IndieSpark",
        };

        public static string ResolvePipeEndpoint(
            string? configuredEndpoint,
            IGPSDKEnvironment sdkEnvironment = IGPSDKEnvironment.PROD,
            Func<string, string?>? environmentReader = null)
        {
            if (!string.IsNullOrWhiteSpace(configuredEndpoint))
            {
                return configuredEndpoint.Trim();
            }

            var environmentEndpoint = ReadEnvironmentVariable(DesktopPipeEndpointEnvironmentVariable, environmentReader);
            if (!string.IsNullOrWhiteSpace(environmentEndpoint))
            {
                return environmentEndpoint.Trim();
            }

            return sdkEnvironment == IGPSDKEnvironment.DEV ? DevPipeEndpoint : ProdPipeEndpoint;
        }

        public static bool TryResolveAppId(
            int configuredAppId,
            int? launchAppId,
            out int appId,
            Func<string, string?>? environmentReader = null)
        {
            if (configuredAppId > 0)
            {
                appId = configuredAppId;
                return true;
            }

            if (launchAppId.HasValue && launchAppId.Value > 0)
            {
                appId = launchAppId.Value;
                return true;
            }

            if (TryParsePositiveInt(ReadEnvironmentVariable(AppIdEnvironmentVariable, environmentReader), out appId) ||
                TryParsePositiveInt(ReadEnvironmentVariable(LegacyAppIdEnvironmentVariable, environmentReader), out appId))
            {
                return true;
            }

            appId = 0;
            return false;
        }

        public static string? ResolveExecutablePath(
            string? overridePath,
            Func<string?>? processPathProvider = null,
            Func<bool>? editorDetector = null)
        {
            if (!string.IsNullOrWhiteSpace(overridePath))
            {
                return NormalizePath(overridePath);
            }

            var processPath = processPathProvider?.Invoke();
            if (string.IsNullOrWhiteSpace(processPath))
            {
                processPath = TryGetCurrentProcessPath();
            }

            return NormalizePath(processPath);
        }

        public static string? ResolveDesktopLaunchCommand(
            string? configuredCommand,
            Func<string, string, string?>? registryReader = null,
            Func<string, string?>? environmentReader = null,
            Func<string?>? localAppDataPathProvider = null,
            Func<string, bool>? fileExists = null,
            Func<string, IGPSDKEnvironment, bool>? trustedDesktopExecutable = null,
            IGPSDKEnvironment sdkEnvironment = IGPSDKEnvironment.PROD)
        {
            if (!string.IsNullOrWhiteSpace(configuredCommand))
            {
                return NormalizeAllowedLaunchCommand(configuredCommand, sdkEnvironment, trustedDesktopExecutable);
            }

            var exists = fileExists ?? File.Exists;
            var environmentPath = ReadEnvironmentVariable(DesktopPathEnvironmentVariable, environmentReader);
            var environmentCommand = NormalizeExistingLaunchCommand(environmentPath, exists, sdkEnvironment, trustedDesktopExecutable);
            if (!string.IsNullOrWhiteSpace(environmentCommand))
            {
                return environmentCommand;
            }

            var appPathsRegistryPath = sdkEnvironment == IGPSDKEnvironment.DEV
                ? DevAppPathsRegistryPath
                : AppPathsRegistryPath;
            var hklmAppPath = NormalizeExistingLaunchCommand(
                ReadRegistryValue("HKLM", appPathsRegistryPath, registryReader),
                exists,
                sdkEnvironment,
                trustedDesktopExecutable);
            if (!string.IsNullOrWhiteSpace(hklmAppPath))
            {
                return hklmAppPath;
            }

            var hkcuAppPath = NormalizeExistingLaunchCommand(
                ReadRegistryValue("HKCU", appPathsRegistryPath, registryReader),
                exists,
                sdkEnvironment,
                trustedDesktopExecutable);
            if (!string.IsNullOrWhiteSpace(hkcuAppPath))
            {
                return hkcuAppPath;
            }

            var curioInstallPath = sdkEnvironment == IGPSDKEnvironment.DEV
                ? ReadRegistryValue("HKCU", IndieSparkDesktopInstallRegistryPath, CurioDesktopInstallRegistryValue, registryReader)
                : ReadRegistryValue("HKCU", CurioDesktopInstallRegistryPath, CurioDesktopInstallRegistryValue, registryReader);
            var curioInstallCommand = BuildLaunchCommandFromInstallPath(
                curioInstallPath,
                GetDesktopExecutableName(sdkEnvironment),
                exists,
                sdkEnvironment,
                trustedDesktopExecutable);
            if (!string.IsNullOrWhiteSpace(curioInstallCommand))
            {
                return curioInstallCommand;
            }

            var installPath = ReadRegistryValue("HKCU", DesktopInstallRegistryPath, DesktopInstallRegistryValue, registryReader);
            var installCommand = BuildLaunchCommandFromInstallPath(
                installPath,
                GetDesktopExecutableName(sdkEnvironment),
                exists,
                sdkEnvironment,
                trustedDesktopExecutable);
            if (!string.IsNullOrWhiteSpace(installCommand))
            {
                return installCommand;
            }

            return ResolveDesktopLaunchCommandFromLocalPrograms(
                localAppDataPathProvider,
                exists,
                trustedDesktopExecutable,
                sdkEnvironment);
        }

        public static ProcessStartInfo CreateDesktopLaunchStartInfo(string command)
        {
            if (string.IsNullOrWhiteSpace(command))
            {
                throw new ArgumentException("Desktop launch command is required", nameof(command));
            }

            ParseCommand(command, out var fileName, out var arguments);
            return new ProcessStartInfo
            {
                FileName = fileName,
                Arguments = arguments,
                UseShellExecute = true,
            };
        }

        internal static string? BuildLaunchCommandFromInstallPath(
            string? installPath,
            string desktopExecutableName,
            Func<string, bool> fileExists,
            IGPSDKEnvironment sdkEnvironment,
            Func<string, IGPSDKEnvironment, bool>? trustedDesktopExecutable)
        {
            if (string.IsNullOrWhiteSpace(installPath))
            {
                return null;
            }

            var trimmed = installPath.Trim().Trim('"');
            if (string.IsNullOrWhiteSpace(trimmed))
            {
                return null;
            }

            var candidate = trimmed.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)
                ? trimmed
                : Path.Combine(trimmed, desktopExecutableName);
            if (!fileExists(candidate))
            {
                return null;
            }

            return NormalizeAllowedLaunchCommand(candidate, sdkEnvironment, trustedDesktopExecutable);
        }

        internal static string? ResolveDesktopLaunchCommandFromLocalPrograms(
            Func<string?>? localAppDataPathProvider = null,
            Func<string, bool>? fileExists = null,
            Func<string, IGPSDKEnvironment, bool>? trustedDesktopExecutable = null,
            IGPSDKEnvironment sdkEnvironment = IGPSDKEnvironment.PROD)
        {
            var localAppDataPath = localAppDataPathProvider?.Invoke();
            if (string.IsNullOrWhiteSpace(localAppDataPath))
            {
                try
                {
                    localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
                }
                catch
                {
                    localAppDataPath = null;
                }
            }

            if (string.IsNullOrWhiteSpace(localAppDataPath))
            {
                return null;
            }

            var exists = fileExists ?? File.Exists;
            var folders = sdkEnvironment == IGPSDKEnvironment.DEV
                ? DevLocalProgramsInstallFolders
                : LocalProgramsInstallFolders;
            var executableName = GetDesktopExecutableName(sdkEnvironment);
            foreach (var folderName in folders)
            {
                var candidate = Path.Combine(localAppDataPath, "Programs", folderName, executableName);
                if (exists(candidate))
                {
                    var command = NormalizeAllowedLaunchCommand(candidate, sdkEnvironment, trustedDesktopExecutable);
                    if (!string.IsNullOrWhiteSpace(command))
                    {
                        return command;
                    }
                }
            }

            return null;
        }

        private static string GetDesktopExecutableName(IGPSDKEnvironment sdkEnvironment)
        {
            return sdkEnvironment == IGPSDKEnvironment.DEV
                ? DevDesktopExecutableName
                : ProdDesktopExecutableName;
        }

        internal static bool IsTrustedDesktopExecutable(string executablePath, IGPSDKEnvironment sdkEnvironment)
        {
            if (sdkEnvironment == IGPSDKEnvironment.DEV)
            {
                return true;
            }

            var expectedName = GetDesktopExecutableName(sdkEnvironment);
            var actualName = Path.GetFileName(executablePath);
            if (!string.Equals(actualName, expectedName, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            if (IsKnownOfficialDesktopInstallPath(executablePath))
            {
                return true;
            }

            try
            {
                using var certificate = new X509Certificate2(X509Certificate.CreateFromSignedFile(executablePath));
                if (!CertificateSubjectLooksOfficial(certificate.Subject))
                {
                    return false;
                }

                using var chain = new X509Chain();
                chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
                chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
                return chain.Build(certificate);
            }
            catch
            {
                return false;
            }
        }

        private static string? TryGetCurrentProcessPath()
        {
            try
            {
                using var process = Process.GetCurrentProcess();
                return process.MainModule?.FileName;
            }
            catch
            {
                return null;
            }
        }

        private static bool IsEditor(Func<bool>? editorDetector)
        {
            if (editorDetector != null)
            {
                return editorDetector();
            }

            var applicationType = Type.GetType("UnityEngine.Application, UnityEngine.CoreModule");
            var property = applicationType?.GetProperty("isEditor", BindingFlags.Public | BindingFlags.Static);
            if (property?.GetValue(null) is bool isEditor)
            {
                return isEditor;
            }

            return false;
        }

        private static string? NormalizePath(string? path)
        {
            if (string.IsNullOrWhiteSpace(path))
            {
                return null;
            }

            var trimmed = path.Trim().Trim('"');
            if (string.IsNullOrWhiteSpace(trimmed))
            {
                return null;
            }

            try
            {
                return Path.GetFullPath(trimmed);
            }
            catch
            {
                return trimmed;
            }
        }

        private static string? NormalizeLaunchCommand(string? command)
        {
            if (string.IsNullOrWhiteSpace(command))
            {
                return null;
            }

            var trimmed = command.Trim();
            if (string.IsNullOrWhiteSpace(trimmed))
            {
                return null;
            }

            ParseCommand(trimmed, out var fileName, out var arguments);
            var normalizedFileName = NormalizePath(fileName) ?? fileName;
            return string.IsNullOrWhiteSpace(arguments)
                ? normalizedFileName
                : $"\"{normalizedFileName}\" {arguments}";
        }

        private static string? NormalizeAllowedLaunchCommand(
            string? command,
            IGPSDKEnvironment sdkEnvironment,
            Func<string, IGPSDKEnvironment, bool>? trustedDesktopExecutable)
        {
            if (string.IsNullOrWhiteSpace(command))
            {
                return null;
            }

            ParseCommand(command.Trim(), out var fileName, out _);
            var normalizedFileName = NormalizePath(fileName) ?? fileName;
            var validator = trustedDesktopExecutable ?? IsTrustedDesktopExecutable;
            if (!validator(normalizedFileName, sdkEnvironment))
            {
                return null;
            }

            return NormalizeLaunchCommand(command);
        }

        private static string? NormalizeExistingLaunchCommand(
            string? command,
            Func<string, bool> fileExists,
            IGPSDKEnvironment sdkEnvironment,
            Func<string, IGPSDKEnvironment, bool>? trustedDesktopExecutable)
        {
            if (string.IsNullOrWhiteSpace(command))
            {
                return null;
            }

            ParseCommand(command.Trim(), out var fileName, out _);
            var normalizedFileName = NormalizePath(fileName) ?? fileName;
            if (!fileExists(normalizedFileName))
            {
                return null;
            }

            return NormalizeAllowedLaunchCommand(command, sdkEnvironment, trustedDesktopExecutable);
        }

        private static bool CertificateSubjectLooksOfficial(string subject)
        {
            foreach (var token in OfficialDesktopCertificateSubjectTokens)
            {
                if (subject.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    return true;
                }
            }

            return false;
        }

        private static bool IsKnownOfficialDesktopInstallPath(string executablePath)
        {
            var normalizedPath = NormalizePath(executablePath) ?? executablePath;
            return IsSamePath(normalizedPath, BuildKnownDesktopPath(Environment.SpecialFolder.LocalApplicationData, "Programs", "IndieGamesPass"))
                || IsSamePath(normalizedPath, BuildKnownDesktopPath(Environment.SpecialFolder.ProgramFiles, "IndieGamesPass"))
                || IsSamePath(normalizedPath, BuildKnownDesktopPath(Environment.SpecialFolder.ProgramFilesX86, "IndieGamesPass"));
        }

        private static string? BuildKnownDesktopPath(Environment.SpecialFolder folder, params string[] segments)
        {
            string? basePath;
            try
            {
                basePath = Environment.GetFolderPath(folder);
            }
            catch
            {
                basePath = null;
            }

            if (string.IsNullOrWhiteSpace(basePath))
            {
                return null;
            }

            var path = basePath;
            foreach (var segment in segments)
            {
                path = Path.Combine(path, segment);
            }

            return Path.Combine(path, ProdDesktopExecutableName);
        }

        private static bool IsSamePath(string path, string? candidate)
        {
            if (string.IsNullOrWhiteSpace(candidate))
            {
                return false;
            }

            var normalizedCandidate = NormalizePath(candidate) ?? candidate;
            return string.Equals(path, normalizedCandidate, StringComparison.OrdinalIgnoreCase);
        }

        private static void ParseCommand(string command, out string fileName, out string arguments)
        {
            var trimmed = command.Trim();
            if (trimmed.StartsWith("\"", StringComparison.Ordinal))
            {
                var closingQuote = trimmed.IndexOf('"', 1);
                if (closingQuote > 0)
                {
                    fileName = trimmed.Substring(1, closingQuote - 1);
                    arguments = trimmed.Substring(closingQuote + 1).Trim();
                    return;
                }
            }

            var exeIndex = trimmed.IndexOf(".exe", StringComparison.OrdinalIgnoreCase);
            if (exeIndex >= 0)
            {
                fileName = trimmed.Substring(0, exeIndex + 4).Trim();
                arguments = trimmed.Substring(exeIndex + 4).Trim();
                return;
            }

            fileName = trimmed;
            arguments = string.Empty;
        }

        private static string? ReadEnvironmentVariable(string name, Func<string, string?>? environmentReader)
        {
            return environmentReader != null
                ? environmentReader(name)
                : Environment.GetEnvironmentVariable(name);
        }

        private static bool TryParsePositiveInt(string? value, out int result)
        {
            if (int.TryParse(value, out result) && result > 0)
            {
                return true;
            }

            result = 0;
            return false;
        }

        private static string? ReadRegistryValue(
            string rootName,
            string subKeyPath,
            Func<string, string, string?>? registryReader)
        {
            return ReadRegistryValue(rootName, subKeyPath, string.Empty, registryReader);
        }

        private static string? ReadRegistryValue(
            string rootName,
            string subKeyPath,
            string valueName,
            Func<string, string, string?>? registryReader)
        {
            if (registryReader != null)
            {
                return registryReader(rootName, $"{subKeyPath}|{valueName}");
            }

            if (!IsWindows())
            {
                return null;
            }

            return TryReadRegistryValue(rootName, subKeyPath, valueName);
        }

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

        private static string? TryReadRegistryValue(string rootName, string subKeyPath, string valueName)
        {
            var directValue = TryReadRegistryValueWithBaseKey(rootName, subKeyPath, valueName);
            if (!string.IsNullOrWhiteSpace(directValue))
            {
                return directValue;
            }

            return TryReadRegistryValueWithReflection(rootName, subKeyPath, valueName);
        }

        private static string? TryReadRegistryValueWithBaseKey(string rootName, string subKeyPath, string valueName)
        {
            try
            {
                var registryKeyType =
                    Type.GetType("Microsoft.Win32.RegistryKey, Microsoft.Win32.Registry")
                    ?? Type.GetType("Microsoft.Win32.RegistryKey, mscorlib");
                var hiveType =
                    Type.GetType("Microsoft.Win32.RegistryHive, Microsoft.Win32.Registry")
                    ?? Type.GetType("Microsoft.Win32.RegistryHive, mscorlib");
                var viewType =
                    Type.GetType("Microsoft.Win32.RegistryView, Microsoft.Win32.Registry")
                    ?? Type.GetType("Microsoft.Win32.RegistryView, mscorlib");
                if (registryKeyType == null || hiveType == null || viewType == null)
                {
                    return null;
                }

                var openBaseKey = registryKeyType.GetMethod(
                    "OpenBaseKey",
                    BindingFlags.Public | BindingFlags.Static,
                    binder: null,
                    types: new[] { hiveType, viewType },
                    modifiers: null);
                var openSubKey = registryKeyType.GetMethod("OpenSubKey", new[] { typeof(string) });
                var getValue = registryKeyType.GetMethod("GetValue", new[] { typeof(string) });
                if (openBaseKey == null || openSubKey == null || getValue == null)
                {
                    return null;
                }

                var hiveName = string.Equals(rootName, "HKLM", StringComparison.OrdinalIgnoreCase)
                    ? "LocalMachine"
                    : "CurrentUser";
                var hive = Enum.Parse(hiveType, hiveName, ignoreCase: false);

                foreach (var view in GetRegistryViewValues(viewType))
                {
                    object? baseKey = null;
                    object? subKey = null;

                    try
                    {
                        baseKey = openBaseKey.Invoke(null, new[] { hive, view });
                        if (baseKey == null)
                        {
                            continue;
                        }

                        subKey = openSubKey.Invoke(baseKey, new object[] { subKeyPath });
                        if (subKey == null)
                        {
                            continue;
                        }

                        var resolvedValueName = string.IsNullOrEmpty(valueName) ? null : valueName;
                        if (getValue.Invoke(subKey, new object?[] { resolvedValueName }) is string stringValue &&
                            !string.IsNullOrWhiteSpace(stringValue))
                        {
                            return stringValue;
                        }
                    }
                    finally
                    {
                        if (subKey is IDisposable disposableSubKey)
                        {
                            disposableSubKey.Dispose();
                        }

                        if (baseKey is IDisposable disposableBaseKey)
                        {
                            disposableBaseKey.Dispose();
                        }
                    }
                }
            }
            catch
            {
            }

            return null;
        }

        private static object[] GetRegistryViewValues(Type viewType)
        {
            object? defaultView = TryParseEnumValue(viewType, "Default");
            if (defaultView == null)
            {
                return Array.Empty<object>();
            }

            try
            {
                if (Environment.Is64BitOperatingSystem)
                {
                    var registry64 = TryParseEnumValue(viewType, "Registry64");
                    var registry32 = TryParseEnumValue(viewType, "Registry32");
                    if (registry64 != null && registry32 != null)
                    {
                        return new[] { registry64, registry32, defaultView };
                    }
                }
            }
            catch
            {
            }

            return new[] { defaultView };
        }

        private static object? TryParseEnumValue(Type enumType, string valueName)
        {
            try
            {
                return Enum.Parse(enumType, valueName, ignoreCase: false);
            }
            catch
            {
                return null;
            }
        }

        private static string? TryReadRegistryValueWithReflection(string rootName, string subKeyPath, string valueName)
        {
            object? subKey = null;

            try
            {
                var registryType =
                    Type.GetType("Microsoft.Win32.Registry, Microsoft.Win32.Registry")
                    ?? Type.GetType("Microsoft.Win32.Registry, mscorlib");
                if (registryType == null)
                {
                    return null;
                }

                var rootPropertyName = string.Equals(rootName, "HKLM", StringComparison.OrdinalIgnoreCase)
                    ? "LocalMachine"
                    : "CurrentUser";
                var root = registryType.GetProperty(rootPropertyName, BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
                if (root == null)
                {
                    return null;
                }

                var openSubKey = root.GetType().GetMethod("OpenSubKey", new[] { typeof(string) });
                if (openSubKey == null)
                {
                    return null;
                }

                subKey = openSubKey.Invoke(root, new object[] { subKeyPath });
                if (subKey == null)
                {
                    return null;
                }

                var getValue = subKey.GetType().GetMethod("GetValue", new[] { typeof(string) });
                return getValue?.Invoke(subKey, new object[] { valueName }) as string;
            }
            catch
            {
                return null;
            }
            finally
            {
                if (subKey is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }
        }
    }
}
