using Microsoft.Win32;
using System.ComponentModel;
using System.Diagnostics;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;

namespace Zcbot.WindowsNode;

internal enum SoftwarePathKind
{
    None,
    Executable,
    Directory,
}

internal sealed record SoftwareDefinition(
    string Id,
    string DisplayName,
    string RuntimeId,
    SoftwarePathKind PathKind,
    string? EnvironmentVariable,
    string? RequiredRelativePath,
    string RequirementsRelativePath);

internal sealed record SoftwareLocation(
    bool Available,
    string? Path,
    string Source,
    string Detail);

internal sealed record RuntimeInstallResult(string RuntimePath, string Detail);

internal static class SoftwareRuntimeManager
{
    private const string SettingsKey = @"Software\Zcbot\WindowsNode\Software";
    private const string DefaultIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple/";

    internal static IReadOnlyList<SoftwareDefinition> Definitions { get; } =
    [
        new(
            "origin",
            "Origin",
            "origin",
            SoftwarePathKind.Executable,
            "ZCBOT_ORIGIN_EXE",
            "Origin*.exe",
            @"adapters\origin.plot@v2\requirements.txt"),
        new(
            "ansys",
            "ANSYS Mechanical 2024 R2",
            "ansys",
            SoftwarePathKind.Directory,
            "AWP_ROOT242",
            @"aisol\bin\winx64\AnsysWBU.exe",
            @"adapters\ansys.mechanical.static_structural@v2\requirements.txt"),
        new(
            "blender",
            "Blender",
            "blender",
            SoftwarePathKind.Executable,
            "ZCBOT_BLENDER_EXE",
            "blender.exe",
            @"adapters\blender.scene.author@v3\requirements.txt"),
    ];

    internal static SoftwareDefinition ByRuntimeId(string runtimeId) =>
        Definitions.Single(item => item.RuntimeId.Equals(runtimeId, StringComparison.Ordinal));

    internal static SoftwareLocation ResolveLocation(SoftwareDefinition definition)
    {
        var configured = ReadConfiguredPath(definition.Id);
        if (!string.IsNullOrWhiteSpace(configured))
        {
            return Location(definition, configured, "界面配置", strict: true);
        }

        if (definition.EnvironmentVariable is not null)
        {
            var environment = Environment.GetEnvironmentVariable(definition.EnvironmentVariable);
            if (!string.IsNullOrWhiteSpace(environment))
            {
                return Location(
                    definition,
                    environment,
                    $"环境变量 {definition.EnvironmentVariable}",
                    strict: true);
            }
        }

        return definition.Id switch
        {
            "origin" => DetectOrigin(definition),
            "ansys" => DetectAnsys(definition),
            "blender" => DetectBlender(definition),
            _ => new SoftwareLocation(false, null, "自动检测", "没有可用的自动检测规则。"),
        };
    }

    internal static void SaveConfiguredPath(SoftwareDefinition definition, string path)
    {
        var location = Location(definition, path, "界面配置", strict: true);
        if (!location.Available || location.Path is null)
        {
            throw new InvalidDataException(location.Detail);
        }
        using var key = Registry.CurrentUser.CreateSubKey(SettingsKey, writable: true)
            ?? throw new InvalidOperationException("无法保存专业软件设置。");
        key.SetValue(definition.Id, location.Path, RegistryValueKind.String);
    }

    internal static void ClearConfiguredPath(SoftwareDefinition definition)
    {
        using var key = Registry.CurrentUser.OpenSubKey(SettingsKey, writable: true);
        key?.DeleteValue(definition.Id, throwOnMissingValue: false);
    }

    internal static void ApplyProcessEnvironment(ProcessStartInfo startInfo, string? runtimeId)
    {
        if (runtimeId is null)
        {
            return;
        }
        var definition = Definitions.SingleOrDefault(
            item => item.RuntimeId.Equals(runtimeId, StringComparison.Ordinal));
        if (definition?.EnvironmentVariable is null)
        {
            return;
        }
        var location = ResolveLocation(definition);
        if (location.Available && location.Path is not null)
        {
            startInfo.Environment[definition.EnvironmentVariable] = location.Path;
        }
    }

    internal static async Task<RuntimeInstallResult> InstallRuntimeAsync(
        SoftwareDefinition definition,
        IProgress<string> progress,
        CancellationToken cancellationToken)
    {
        var location = ResolveLocation(definition);
        if (!location.Available)
        {
            throw new InvalidOperationException(
                $"尚未检测到 {definition.DisplayName}：{location.Detail}");
        }

        var requirements = ResolveBundledFile(definition.RequirementsRelativePath);
        var root = NodePaths.ForCurrentMachine().RootDirectory;
        var runtimes = Path.Combine(root, "runtimes");
        Directory.CreateDirectory(runtimes);
        NodeConfigStore.RestrictDirectory(root);

        var runtime = Path.Combine(runtimes, definition.RuntimeId);
        var staging = Path.Combine(
            runtimes,
            $".{definition.RuntimeId}.installing-{Guid.NewGuid():N}");
        var backup = Path.Combine(
            runtimes,
            $".{definition.RuntimeId}.rollback-{Guid.NewGuid():N}");
        var stagingPython = Path.Combine(staging, "Scripts", "python.exe");
        var movedExisting = false;
        try
        {
            progress.Report("正在查找 Python 3.12…");
            var bootstrap = await FindPython312Async(cancellationToken);
            progress.Report("正在创建隔离运行环境…");
            await RunAsync(
                bootstrap.FileName,
                bootstrap.PrefixArguments.Concat(["-m", "venv", staging]),
                AppContext.BaseDirectory,
                progress,
                cancellationToken);
            if (!File.Exists(stagingPython))
            {
                throw new InvalidDataException("Python venv 未生成 Scripts\\python.exe。");
            }

            await VerifyPython312Async(stagingPython, cancellationToken);
            progress.Report("正在安装 adapter 依赖…");
            var indexUrl = Environment.GetEnvironmentVariable("ZCBOT_PIP_INDEX_URL");
            if (string.IsNullOrWhiteSpace(indexUrl))
            {
                indexUrl = DefaultIndexUrl;
            }
            await RunAsync(
                stagingPython,
                ["-m", "pip", "install", "--index-url", indexUrl, "--requirement", requirements],
                AppContext.BaseDirectory,
                progress,
                cancellationToken);

            if (Directory.Exists(runtime))
            {
                Directory.Move(runtime, backup);
                movedExisting = true;
            }
            Directory.Move(staging, runtime);
            if (movedExisting)
            {
                Directory.Delete(backup, recursive: true);
            }
            NodeConfigStore.RestrictDirectory(runtime);
            return new RuntimeInstallResult(
                Path.Combine(runtime, "Scripts", "python.exe"),
                $"{definition.DisplayName} 运行环境安装完成。");
        }
        catch
        {
            if (movedExisting && !Directory.Exists(runtime) && Directory.Exists(backup))
            {
                Directory.Move(backup, runtime);
            }
            throw;
        }
        finally
        {
            DeleteDirectoryIfPresent(staging);
            DeleteDirectoryIfPresent(backup);
        }
    }

    private static SoftwareLocation DetectOrigin(SoftwareDefinition definition)
    {
        var candidates = new List<(string Path, string Source)>();
        try
        {
            using var clsid = Registry.ClassesRoot.OpenSubKey(@"Origin.ApplicationSI\CLSID");
            var clsidValue = clsid?.GetValue(null) as string;
            if (!string.IsNullOrWhiteSpace(clsidValue))
            {
                using var server = Registry.ClassesRoot.OpenSubKey($@"CLSID\{clsidValue}\LocalServer32");
                var executable = ExecutableFromCommand(server?.GetValue(null) as string);
                if (executable is not null)
                {
                    candidates.Add((executable, "COM 注册"));
                }
            }
        }
        catch (Exception exception) when (
            exception is IOException or SecurityException or UnauthorizedAccessException)
        {
            // Continue with uninstall metadata and standard install directories.
        }
        candidates.AddRange(UninstallLocations("Origin")
            .SelectMany(OriginExecutables)
            .Select(path => (path, "注册表")));
        foreach (var variable in new[] { "ProgramFiles", "ProgramW6432" })
        {
            var root = Environment.GetEnvironmentVariable(variable);
            if (string.IsNullOrWhiteSpace(root)) continue;
            var originRoot = Path.Combine(root, "OriginLab");
            if (!Directory.Exists(originRoot)) continue;
            try
            {
                candidates.AddRange(Directory.EnumerateDirectories(originRoot)
                    .OrderByDescending(item => item, StringComparer.OrdinalIgnoreCase)
                    .SelectMany(OriginExecutables)
                    .Select(path => (path, "标准目录")));
            }
            catch (Exception exception) when (
                exception is IOException or SecurityException or UnauthorizedAccessException)
            {
                // Continue with candidates already collected from other sources.
            }
        }
        return FirstLocation(definition, candidates);
    }

    private static IEnumerable<string> OriginExecutables(string location)
    {
        if (File.Exists(location))
        {
            yield return location;
            yield break;
        }
        if (!Directory.Exists(location)) yield break;
        IEnumerable<string> files;
        try
        {
            files = Directory.EnumerateFiles(location, "Origin*.exe", SearchOption.TopDirectoryOnly)
                .OrderByDescending(item => item, StringComparer.OrdinalIgnoreCase)
                .ToArray();
        }
        catch (Exception exception) when (
            exception is IOException or SecurityException or UnauthorizedAccessException)
        {
            yield break;
        }
        foreach (var file in files) yield return file;
    }

    private static SoftwareLocation DetectAnsys(SoftwareDefinition definition)
    {
        var candidates = new List<(string Path, string Source)>();
        foreach (var location in UninstallLocations("ANSYS"))
        {
            candidates.Add((location, "注册表"));
            candidates.Add((Path.Combine(location, "v242"), "注册表"));
        }
        candidates.Add((@"C:\Program Files\ANSYS Inc\v242", "标准目录"));
        return FirstLocation(definition, candidates);
    }

    private static SoftwareLocation DetectBlender(SoftwareDefinition definition)
    {
        var candidates = new List<(string Path, string Source)>();
        var path = FindOnPath("blender.exe");
        if (path is not null)
        {
            candidates.Add((path, "PATH"));
        }
        candidates.AddRange(UninstallLocations("Blender")
            .Select(location => (Path.Combine(location, "blender.exe"), "注册表")));
        foreach (var variable in new[] { "ProgramFiles", "ProgramW6432" })
        {
            var root = Environment.GetEnvironmentVariable(variable);
            if (string.IsNullOrWhiteSpace(root)) continue;
            var blenderRoot = Path.Combine(root, "Blender Foundation");
            if (!Directory.Exists(blenderRoot)) continue;
            candidates.AddRange(Directory.EnumerateDirectories(blenderRoot, "Blender *")
                .OrderByDescending(item => item, StringComparer.OrdinalIgnoreCase)
                .Select(item => (Path.Combine(item, "blender.exe"), "标准目录")));
        }
        return FirstLocation(definition, candidates);
    }

    private static SoftwareLocation FirstLocation(
        SoftwareDefinition definition,
        IEnumerable<(string Path, string Source)> candidates)
    {
        foreach (var candidate in candidates)
        {
            var location = Location(definition, candidate.Path, candidate.Source, strict: false);
            if (location.Available)
            {
                return location;
            }
        }
        return new SoftwareLocation(false, null, "自动检测", "没有找到有效的安装位置。");
    }

    private static SoftwareLocation Location(
        SoftwareDefinition definition,
        string value,
        string source,
        bool strict)
    {
        try
        {
            var path = Path.GetFullPath(Environment.ExpandEnvironmentVariables(value.Trim().Trim('"')));
            var required = definition.PathKind switch
            {
                SoftwarePathKind.Executable => path,
                SoftwarePathKind.Directory => Path.Combine(path, definition.RequiredRelativePath!),
                _ => path,
            };
            var valid = File.Exists(required)
                && (definition.PathKind != SoftwarePathKind.Executable
                    || MatchesExecutable(
                        Path.GetFileName(required),
                        definition.RequiredRelativePath!));
            return valid
                ? new SoftwareLocation(true, path, source, $"已检测到 {required}")
                : new SoftwareLocation(
                    false,
                    strict ? path : null,
                    source,
                    $"指定位置缺少 {definition.RequiredRelativePath}。");
        }
        catch (Exception exception) when (
            exception is ArgumentException or IOException or NotSupportedException or SecurityException)
        {
            return new SoftwareLocation(false, null, source, exception.Message);
        }
    }

    private static bool MatchesExecutable(string filename, string pattern)
    {
        if (!pattern.Contains('*'))
        {
            return filename.Equals(pattern, StringComparison.OrdinalIgnoreCase);
        }
        var parts = pattern.Split('*', 2);
        return filename.StartsWith(parts[0], StringComparison.OrdinalIgnoreCase)
            && filename.EndsWith(parts[1], StringComparison.OrdinalIgnoreCase)
            && filename.Length >= parts[0].Length + parts[1].Length;
    }

    private static string? ReadConfiguredPath(string id)
    {
        using var key = Registry.CurrentUser.OpenSubKey(SettingsKey);
        return key?.GetValue(id) as string;
    }

    private static IReadOnlyList<string> UninstallLocations(string displayNameFragment)
    {
        var result = new List<string>();
        foreach (var hive in new[] { RegistryHive.LocalMachine, RegistryHive.CurrentUser })
        {
            foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
            {
                RegistryKey? baseKey = null;
                RegistryKey? uninstall = null;
                try
                {
                    baseKey = RegistryKey.OpenBaseKey(hive, view);
                    uninstall = baseKey.OpenSubKey(
                        @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall");
                    if (uninstall is null) continue;
                    foreach (var name in uninstall.GetSubKeyNames())
                    {
                        using var entry = uninstall.OpenSubKey(name);
                        var displayName = entry?.GetValue("DisplayName") as string;
                        var location = entry?.GetValue("InstallLocation") as string;
                        if (!string.IsNullOrWhiteSpace(displayName)
                            && displayName.Contains(displayNameFragment, StringComparison.OrdinalIgnoreCase)
                            && !string.IsNullOrWhiteSpace(location))
                        {
                            result.Add(location);
                        }
                    }
                }
                catch (Exception exception) when (
                    exception is IOException or SecurityException or UnauthorizedAccessException)
                {
                    // A registry view can be unavailable to the current node account.
                }
                finally
                {
                    uninstall?.Dispose();
                    baseKey?.Dispose();
                }
            }
        }
        return result.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
    }

    private static string? FindOnPath(string filename)
    {
        var path = Environment.GetEnvironmentVariable("PATH");
        if (string.IsNullOrWhiteSpace(path)) return null;
        foreach (var directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
        {
            try
            {
                var candidate = Path.Combine(directory.Trim().Trim('"'), filename);
                if (File.Exists(candidate)) return Path.GetFullPath(candidate);
            }
            catch (Exception exception) when (
                exception is ArgumentException or IOException or NotSupportedException)
            {
                // Ignore malformed PATH entries and continue with the next one.
            }
        }
        return null;
    }

    private static string? ExecutableFromCommand(string? command)
    {
        if (string.IsNullOrWhiteSpace(command)) return null;
        var value = Environment.ExpandEnvironmentVariables(command.Trim().TrimEnd('\0'));
        var match = Regex.Match(
            value,
            "^\\s*(?:\"(?<quoted>[^\"]+\\.exe)\"|(?<plain>.+?\\.exe))(?:\\s|$)",
            RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        if (!match.Success) return null;
        var candidate = match.Groups["quoted"].Success
            ? match.Groups["quoted"].Value
            : match.Groups["plain"].Value;
        return File.Exists(candidate) ? Path.GetFullPath(candidate) : null;
    }

    private static string ResolveBundledFile(string relativePath)
    {
        var root = Path.GetFullPath(AppContext.BaseDirectory);
        var path = Path.GetFullPath(Path.Combine(root, relativePath));
        if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase) || !File.Exists(path))
        {
            throw new InvalidDataException("Adapter requirements 文件缺失或路径非法。");
        }
        return path;
    }

    private static async Task<BootstrapPython> FindPython312Async(
        CancellationToken cancellationToken)
    {
        var candidates = new[]
        {
            new BootstrapPython("py.exe", ["-3.12"]),
            new BootstrapPython("python.exe", []),
        };
        foreach (var candidate in candidates)
        {
            try
            {
                await RunAsync(
                    candidate.FileName,
                    candidate.PrefixArguments.Concat(
                        ["-c", "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 12) else 1)"]),
                    AppContext.BaseDirectory,
                    progress: null,
                    cancellationToken);
                return candidate;
            }
            catch (Exception exception) when (
                exception is InvalidOperationException or Win32Exception)
            {
                // Try the next fixed Python 3.12 discovery method.
            }
        }
        throw new InvalidOperationException(
            "未找到 Python 3.12。请先安装 Python 3.12 并启用 py launcher 或加入 PATH。");
    }

    private static Task VerifyPython312Async(string python, CancellationToken cancellationToken) =>
        RunAsync(
            python,
            ["-c", "import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 12) else 1)"],
            AppContext.BaseDirectory,
            progress: null,
            cancellationToken);

    private static async Task RunAsync(
        string filename,
        IEnumerable<string> arguments,
        string workingDirectory,
        IProgress<string>? progress,
        CancellationToken cancellationToken)
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = filename,
            WorkingDirectory = workingDirectory,
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            StandardOutputEncoding = Encoding.UTF8,
            StandardErrorEncoding = Encoding.UTF8,
        };
        foreach (var argument in arguments)
        {
            startInfo.ArgumentList.Add(argument);
        }
        startInfo.Environment["PYTHONUTF8"] = "1";
        startInfo.Environment["PYTHONIOENCODING"] = "utf-8";
        using var process = Process.Start(startInfo)
            ?? throw new InvalidOperationException("运行环境安装进程未启动。");
        var stdout = RelayAsync(process.StandardOutput, progress);
        var stderr = RelayAsync(process.StandardError, progress);
        try
        {
            await process.WaitForExitAsync(cancellationToken);
        }
        catch (OperationCanceledException)
        {
            if (!process.HasExited) process.Kill(entireProcessTree: true);
            throw;
        }
        await Task.WhenAll(stdout, stderr);
        if (process.ExitCode != 0)
        {
            throw new InvalidOperationException(
                $"运行环境安装进程失败，退出码 {process.ExitCode}。");
        }
    }

    private static async Task RelayAsync(StreamReader reader, IProgress<string>? progress)
    {
        while (await reader.ReadLineAsync() is { } line)
        {
            if (!string.IsNullOrWhiteSpace(line)) progress?.Report(line);
        }
    }

    private static void DeleteDirectoryIfPresent(string path)
    {
        try
        {
            if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
        }
        catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
        {
            // A successful runtime swap must not be reported as failed only because
            // antivirus software still holds a handle to the disposable directory.
        }
    }

    private sealed record BootstrapPython(
        string FileName,
        IReadOnlyList<string> PrefixArguments);
}
