using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Downloads;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Net;

namespace YMhut.Box.Core.DevEnvironments;

public enum DevEnvironmentInstallMode
{
    QuickInstall,
    SourceBuild
}

public sealed record DevEnvironmentDefinition(
    string Id,
    string Name,
    string CommandName,
    string VersionArgument,
    string OfficialHome,
    string VersionSource,
    string WindowsAssetHint,
    string SourceAssetHint = "source")
{
    public static IReadOnlyList<DevEnvironmentDefinition> Defaults { get; } =
    [
        new("go", "Go", "go", "version", "https://go.dev/dl/", "https://go.dev/dl/?mode=json", "windows-amd64.msi", "src.tar.gz"),
        new("python", "Python", "python", "--version", "https://www.python.org/downloads/windows/", "https://www.python.org/ftp/python/", "amd64.exe", "tgz"),
        new("java", "Java / Temurin JDK", "java", "-version", "https://adoptium.net/temurin/releases/", "https://api.adoptium.net/v3/assets/latest/21/hotspot?os=windows&architecture=x64&image_type=jdk", "jdk_x64_windows_hotspot", "sources"),
        new("docker", "Docker Desktop", "docker", "--version", "https://www.docker.com/products/docker-desktop/", "https://desktop.docker.com/win/main/amd64/appcast.xml", "Docker Desktop Installer.exe"),
        new("mysql", "MySQL", "mysql", "--version", "https://dev.mysql.com/downloads/mysql/", "https://dev.mysql.com/downloads/mysql/", "mysql-installer"),
        new("node", "Node.js", "node", "--version", "https://nodejs.org/en/download", "https://nodejs.org/dist/index.json", "win-x64.msi", "tar.gz"),
        new("dotnet", ".NET SDK", "dotnet", "--list-sdks", "https://dotnet.microsoft.com/download", "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/releases-index.json", "win-x64.exe", "source"),
        new("git", "Git", "git", "--version", "https://git-scm.com/download/win", "https://api.github.com/repos/git-for-windows/git/releases", "64-bit.exe", "tar.gz"),
        new("rust", "Rust", "rustc", "--version", "https://www.rust-lang.org/tools/install", "https://static.rust-lang.org/dist/channel-rust-stable.toml", "rustup-init.exe", "tar.gz"),
        new("cmake", "CMake", "cmake", "--version", "https://cmake.org/download/", "https://api.github.com/repos/Kitware/CMake/releases", "windows-x86_64.msi", "tar.gz")
    ];
}

public sealed record DetectedDevEnvironment(
    string Id,
    string Name,
    bool IsInstalled,
    string Version = "",
    string Path = "",
    string Source = "",
    string Error = "");

public sealed record DevEnvironmentDownloadCandidate(
    string Id,
    string DisplayName,
    DownloadSource Source,
    DevEnvironmentInstallMode Mode,
    string SourceType = "Official",
    string Region = "",
    bool IsDefault = false);

public sealed record DevEnvironmentVersion(
    string Version,
    DownloadSource? Installer,
    DownloadSource? SourceArchive,
    DateTimeOffset? PublishedAt = null,
    string ReleaseNotesUrl = "",
    IReadOnlyList<DevEnvironmentDownloadCandidate>? Candidates = null)
{
    public IReadOnlyList<DevEnvironmentDownloadCandidate> AllCandidates
    {
        get
        {
            if (Candidates is { Count: > 0 })
            {
                return Candidates;
            }

            var list = new List<DevEnvironmentDownloadCandidate>();
            if (Installer is not null)
            {
                list.Add(new DevEnvironmentDownloadCandidate("installer", Installer.DisplayName, Installer, DevEnvironmentInstallMode.QuickInstall, Installer.SourceKind, Installer.MirrorRegion, true));
            }
            if (SourceArchive is not null)
            {
                list.Add(new DevEnvironmentDownloadCandidate("source", SourceArchive.DisplayName, SourceArchive, DevEnvironmentInstallMode.SourceBuild, SourceArchive.SourceKind, SourceArchive.MirrorRegion));
            }

            return list;
        }
    }
}

public sealed record DevEnvironmentBuildRecipe(
    string EnvironmentId,
    string Summary,
    IReadOnlyList<string> Prerequisites,
    IReadOnlyList<string> Commands,
    bool IsSupported = true);

public sealed record DevEnvironmentBuildSession(
    string Id,
    string EnvironmentId,
    string WorkingDirectory,
    string SourceArchivePath,
    DevEnvironmentBuildRecipe Recipe,
    DateTimeOffset CreatedAt);

public sealed record DevEnvironmentInstallPlan(
    string EnvironmentId,
    string EnvironmentName,
    DevEnvironmentVersion Version,
    DevEnvironmentInstallMode Mode,
    string BuildRecipe = "",
    string InstallCommand = "",
    string InstallArguments = "");

public interface IDevEnvironmentDetectionService
{
    Task<IReadOnlyList<DetectedDevEnvironment>> DetectAsync(CancellationToken cancellationToken = default);
}

public interface IDevEnvironmentCatalogService
{
    IReadOnlyList<DevEnvironmentDefinition> Definitions { get; }

    Task<IReadOnlyList<DevEnvironmentVersion>> GetVersionsAsync(string environmentId, CancellationToken cancellationToken = default);

    DevEnvironmentInstallPlan CreateInstallPlan(string environmentId, DevEnvironmentVersion version, DevEnvironmentInstallMode mode);

    DevEnvironmentInstallPlan CreateInstallPlan(string environmentId, DevEnvironmentVersion version, DevEnvironmentDownloadCandidate candidate);
}

public sealed class DevEnvironmentDetectionService(ILogService? logService = null) : IDevEnvironmentDetectionService
{
    public async Task<IReadOnlyList<DetectedDevEnvironment>> DetectAsync(CancellationToken cancellationToken = default)
    {
        var tasks = DevEnvironmentDefinition.Defaults.Select(definition => DetectOneAsync(definition, cancellationToken));
        return await Task.WhenAll(tasks).ConfigureAwait(false);
    }

    private async Task<DetectedDevEnvironment> DetectOneAsync(DevEnvironmentDefinition definition, CancellationToken cancellationToken)
    {
        try
        {
            var result = await RunProcessAsync(definition.CommandName, definition.VersionArgument, cancellationToken).ConfigureAwait(false);
            if (result.ExitCode != 0 && string.IsNullOrWhiteSpace(result.Output))
            {
                return new DetectedDevEnvironment(definition.Id, definition.Name, false, Error: result.Error);
            }

            var version = DevEnvironmentVersionParser.Parse(definition.Id, result.Output);
            var path = await ResolveCommandPathAsync(definition.CommandName, cancellationToken).ConfigureAwait(false);
            return new DetectedDevEnvironment(definition.Id, definition.Name, true, version, path, "PATH");
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception exception)
        {
            await (logService?.WriteAsync("Debug", "dev-env", $"Detect {definition.Id} failed", exception.Message, cancellationToken) ?? Task.CompletedTask)
                .ConfigureAwait(false);
            return new DetectedDevEnvironment(definition.Id, definition.Name, false, Error: exception.Message);
        }
    }

    private static async Task<(int ExitCode, string Output, string Error)> RunProcessAsync(string fileName, string arguments, CancellationToken cancellationToken)
    {
        using var process = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = fileName,
                Arguments = arguments,
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true
            },
            EnableRaisingEvents = true
        };

        process.Start();
        var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
        var errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
        await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
        var output = (await outputTask.ConfigureAwait(false) + Environment.NewLine + await errorTask.ConfigureAwait(false)).Trim();
        return (process.ExitCode, output, string.Empty);
    }

    private static async Task<string> ResolveCommandPathAsync(string command, CancellationToken cancellationToken)
    {
        try
        {
            var result = await RunProcessAsync("where.exe", command, cancellationToken).ConfigureAwait(false);
            return result.Output
                .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                .FirstOrDefault() ?? string.Empty;
        }
        catch
        {
            return string.Empty;
        }
    }
}

public static partial class DevEnvironmentVersionParser
{
    public static string Parse(string id, string output)
    {
        if (string.IsNullOrWhiteSpace(output))
        {
            return string.Empty;
        }

        return id switch
        {
            "go" => Match(output, @"go(?:\s+version)?\s+go(?<v>[0-9][^\s]+)"),
            "python" => Match(output, @"Python\s+(?<v>[0-9][^\s]+)"),
            "java" => Match(output, @"(?:openjdk|java)\s+version\s+""(?<v>[^""]+)""", RegexOptions.IgnoreCase),
            "docker" => Match(output, @"Docker version\s+(?<v>[0-9][^,\s]+)"),
            "mysql" => Match(output, @"Distrib\s+(?<v>[0-9][^,\s]+)|Ver\s+(?<v2>[0-9][^\s]+)"),
            "node" => Match(output, @"v(?<v>[0-9][^\s]+)"),
            "dotnet" => output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault() ?? string.Empty,
            "git" => Match(output, @"git version\s+(?<v>[0-9][^\s]+)"),
            "rust" => Match(output, @"rustc\s+(?<v>[0-9][^\s]+)"),
            "cmake" => Match(output, @"cmake version\s+(?<v>[0-9][^\s]+)"),
            _ => output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault() ?? string.Empty
        };
    }

    private static string Match(string output, string pattern, RegexOptions options = RegexOptions.None)
    {
        var match = Regex.Match(output, pattern, options | RegexOptions.CultureInvariant);
        if (!match.Success)
        {
            return output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault() ?? string.Empty;
        }

        return FirstGroup(match, "v", "v2");
    }

    private static string FirstGroup(Match match, params string[] names)
    {
        foreach (var name in names)
        {
            if (match.Groups[name] is { Success: true } group)
            {
                return group.Value;
            }
        }

        return string.Empty;
    }
}

public sealed class DevEnvironmentCatalogService(
    AppPaths paths,
    IHttpService? httpService = null,
    ILogService? logService = null) : IDevEnvironmentCatalogService
{
    public IReadOnlyList<DevEnvironmentDefinition> Definitions => DevEnvironmentDefinition.Defaults;

    public async Task<IReadOnlyList<DevEnvironmentVersion>> GetVersionsAsync(string environmentId, CancellationToken cancellationToken = default)
    {
        var definition = Definitions.FirstOrDefault(item => string.Equals(item.Id, environmentId, StringComparison.OrdinalIgnoreCase));
        if (definition is null)
        {
            return [];
        }

        var cached = await TryReadCacheAsync(environmentId, cancellationToken).ConfigureAwait(false);
        if (cached.Count > 0)
        {
            return cached;
        }

        try
        {
            var versions = await FetchVersionsAsync(definition, cancellationToken).ConfigureAwait(false);
            if (versions.Count > 0)
            {
                await WriteCacheAsync(environmentId, versions, cancellationToken).ConfigureAwait(false);
                return versions;
            }
        }
        catch (Exception exception)
        {
            await (logService?.WriteAsync("Warning", "dev-env", $"Fetch versions failed: {environmentId}", exception.Message, cancellationToken) ?? Task.CompletedTask)
                .ConfigureAwait(false);
        }

        return OfflineFallback(definition);
    }

    public DevEnvironmentInstallPlan CreateInstallPlan(string environmentId, DevEnvironmentVersion version, DevEnvironmentInstallMode mode)
    {
        var candidate = version.AllCandidates.FirstOrDefault(item => item.Mode == mode) ??
            version.AllCandidates.FirstOrDefault();
        if (candidate is not null)
        {
            return CreateInstallPlan(environmentId, version, candidate);
        }

        var source = mode == DevEnvironmentInstallMode.SourceBuild
            ? version.SourceArchive ?? version.Installer
            : version.Installer ?? version.SourceArchive;
        candidate = source is null
            ? new DevEnvironmentDownloadCandidate("manual", version.Version, new DownloadSource(string.Empty, version.Version, "download.url"), mode)
            : new DevEnvironmentDownloadCandidate(mode == DevEnvironmentInstallMode.SourceBuild ? "source" : "installer", source.DisplayName, source, mode);
        return CreateInstallPlan(environmentId, version, candidate);
    }

    public DevEnvironmentInstallPlan CreateInstallPlan(string environmentId, DevEnvironmentVersion version, DevEnvironmentDownloadCandidate candidate)
    {
        var definition = Definitions.FirstOrDefault(item => string.Equals(item.Id, environmentId, StringComparison.OrdinalIgnoreCase))
            ?? new DevEnvironmentDefinition(environmentId, environmentId, environmentId, "--version", string.Empty, string.Empty, string.Empty);

        if (candidate.Mode == DevEnvironmentInstallMode.SourceBuild)
        {
            var recipe = BuildRecipe(definition);
            return new DevEnvironmentInstallPlan(
                definition.Id,
                definition.Name,
                version,
                candidate.Mode,
                string.Join(Environment.NewLine, recipe.Commands.Prepend(recipe.Summary)),
                "cmd.exe",
                "/k echo Build recipe is ready. Download the source archive first, then follow the listed commands.");
        }

        return new DevEnvironmentInstallPlan(
            definition.Id,
            definition.Name,
            version,
            candidate.Mode,
            InstallCommand: "installer",
            InstallArguments: string.Empty);
    }

    private async Task<IReadOnlyList<DevEnvironmentVersion>> FetchVersionsAsync(DevEnvironmentDefinition definition, CancellationToken cancellationToken)
    {
        if (httpService is null)
        {
            return OfflineFallback(definition);
        }

        var response = await httpService.GetAsync(new Uri(definition.VersionSource), cancellationToken).ConfigureAwait(false);
        return definition.Id switch
        {
            "go" => ParseGo(definition, response.Content),
            "node" => ParseNode(definition, response.Content),
            "dotnet" => ParseDotNet(definition, response.Content),
            "git" => ParseGitHub(definition, response.Content, "Git for Windows", static name =>
                name.EndsWith("64-bit.exe", StringComparison.OrdinalIgnoreCase) ||
                name.Contains("64-bit.exe", StringComparison.OrdinalIgnoreCase)),
            "cmake" => ParseGitHub(definition, response.Content, "CMake", static name =>
                name.Contains("windows-x86_64.msi", StringComparison.OrdinalIgnoreCase) ||
                name.Contains("windows-x86_64.zip", StringComparison.OrdinalIgnoreCase)),
            "rust" => ParseRust(definition, response.Content),
            "python" => ParsePython(definition, response.Content),
            "java" => ParseAdoptium(definition, response.Content),
            _ => OfflineFallback(definition)
        };
    }

    private async Task<IReadOnlyList<DevEnvironmentVersion>> TryReadCacheAsync(string environmentId, CancellationToken cancellationToken)
    {
        try
        {
            var path = CachePath(environmentId);
            if (!File.Exists(path) || DateTimeOffset.UtcNow - File.GetLastWriteTimeUtc(path) > TimeSpan.FromHours(6))
            {
                return [];
            }

            await using var stream = File.OpenRead(path);
            return await JsonSerializer.DeserializeAsync<List<DevEnvironmentVersion>>(stream, cancellationToken: cancellationToken).ConfigureAwait(false) ?? [];
        }
        catch
        {
            return [];
        }
    }

    private async Task WriteCacheAsync(string environmentId, IReadOnlyList<DevEnvironmentVersion> versions, CancellationToken cancellationToken)
    {
        try
        {
            Directory.CreateDirectory(Path.GetDirectoryName(CachePath(environmentId))!);
            await using var stream = File.Create(CachePath(environmentId));
            await JsonSerializer.SerializeAsync(stream, versions.Take(30).ToArray(), cancellationToken: cancellationToken).ConfigureAwait(false);
        }
        catch
        {
        }
    }

    private string CachePath(string environmentId)
    {
        return Path.Combine(paths.Cache, "DevEnvironments", $"{environmentId}.json");
    }

    private static IReadOnlyList<DevEnvironmentVersion> ParseGo(DevEnvironmentDefinition definition, string json)
    {
        using var document = JsonDocument.Parse(json);
        var results = new List<DevEnvironmentVersion>();
        foreach (var release in document.RootElement.EnumerateArray().Take(12))
        {
            var version = release.GetProperty("version").GetString() ?? string.Empty;
            if (string.IsNullOrWhiteSpace(version) || !release.TryGetProperty("files", out var files))
            {
                continue;
            }

            DownloadSource? installer = null;
            DownloadSource? source = null;
            foreach (var file in files.EnumerateArray())
            {
                var name = file.GetProperty("filename").GetString() ?? string.Empty;
                var url = $"https://go.dev/dl/{name}";
                if (name.Contains("windows-amd64.msi", StringComparison.OrdinalIgnoreCase))
                {
                    installer = OfficialSource(url, $"{definition.Name} {version}", name, Sha256: GetString(file, "sha256"), sizeBytes: GetLong(file, "size"));
                }
                else if (name.Contains("src.tar.gz", StringComparison.OrdinalIgnoreCase))
                {
                    source = OfficialSource(url, $"{definition.Name} {version} source", name, Sha256: GetString(file, "sha256"), sizeBytes: GetLong(file, "size"));
                }
            }

            var cleanVersion = version.TrimStart('g', 'o');
            results.Add(WithCandidates(definition.Id, new DevEnvironmentVersion(cleanVersion, installer, source)));
        }

        return results;
    }

    private static IReadOnlyList<DevEnvironmentVersion> ParseNode(DevEnvironmentDefinition definition, string json)
    {
        using var document = JsonDocument.Parse(json);
        return document.RootElement.EnumerateArray()
            .Take(20)
            .Select(item =>
            {
                var version = (item.GetProperty("version").GetString() ?? string.Empty).TrimStart('v');
                var installerName = $"node-v{version}-x64.msi";
                var sourceName = $"node-v{version}.tar.gz";
                return new DevEnvironmentVersion(
                    version,
                    OfficialSource($"https://nodejs.org/dist/v{version}/{installerName}", $"{definition.Name} {version}", installerName),
                    OfficialSource($"https://nodejs.org/dist/v{version}/{sourceName}", $"{definition.Name} {version} source", sourceName),
                    TryDate(GetString(item, "date")));
            })
            .Select(version => WithCandidates(definition.Id, version))
            .ToArray();
    }

    private static IReadOnlyList<DevEnvironmentVersion> ParseDotNet(DevEnvironmentDefinition definition, string json)
    {
        using var document = JsonDocument.Parse(json);
        if (!document.RootElement.TryGetProperty("releases-index", out var releases))
        {
            return [];
        }

        return releases.EnumerateArray()
            .Take(10)
            .Select(item =>
            {
                var version = GetString(item, "latest-sdk");
                var channel = GetString(item, "channel-version");
                var installerName = $"dotnet-sdk-{version}-win-x64.exe";
                var url = $"https://dotnet.microsoft.com/download/dotnet/thank-you/sdk-{version}-windows-x64-installer";
                return new DevEnvironmentVersion(
                    string.IsNullOrWhiteSpace(version) ? channel : version,
                    OfficialSource(url, $"{definition.Name} {version}", installerName),
                    null,
                    TryDate(GetString(item, "release-date")));
            })
            .Where(item => !string.IsNullOrWhiteSpace(item.Version))
            .Select(version => WithCandidates(definition.Id, version))
            .ToArray();
    }

    private static IReadOnlyList<DevEnvironmentVersion> ParseGitHub(
        DevEnvironmentDefinition definition,
        string json,
        string displayPrefix,
        Func<string, bool> installerPredicate)
    {
        using var document = JsonDocument.Parse(json);
        var releases = document.RootElement.ValueKind == JsonValueKind.Array
            ? document.RootElement.EnumerateArray()
            : [];
        return releases
            .Take(20)
            .Select(release =>
            {
                var tag = GetString(release, "tag_name");
                var version = tag.TrimStart('v');
                var published = TryDate(GetString(release, "published_at"));
                DownloadSource? installer = null;
                DownloadSource? source = null;
                if (release.TryGetProperty("assets", out var assets) && assets.ValueKind == JsonValueKind.Array)
                {
                    foreach (var asset in assets.EnumerateArray())
                    {
                        var name = GetString(asset, "name");
                        var url = GetString(asset, "browser_download_url");
                        if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(url))
                        {
                            continue;
                        }

                        if (installer is null && installerPredicate(name))
                        {
                            installer = new DownloadSource(url, $"{displayPrefix} {version}", name, "GitHub", SourceLabel: "GitHub", SizeBytes: GetLong(asset, "size"));
                        }
                        else if (source is null && (name.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) || name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)))
                        {
                            source = new DownloadSource(url, $"{displayPrefix} {version} source", name, "GitHub", SourceLabel: "GitHub", SizeBytes: GetLong(asset, "size"));
                        }
                    }
                }

                var notes = GetString(release, "html_url");
                return WithCandidates(definition.Id, new DevEnvironmentVersion(version, installer, source, published, notes));
            })
            .Where(item => !string.IsNullOrWhiteSpace(item.Version) && item.AllCandidates.Count > 0)
            .ToArray();
    }

    private static IReadOnlyList<DevEnvironmentVersion> ParseRust(DevEnvironmentDefinition definition, string _)
    {
        var installer = OfficialSource(
            "https://win.rustup.rs/x86_64",
            "Rust stable rustup-init",
            "rustup-init.exe");
        return [WithCandidates(definition.Id, new DevEnvironmentVersion("stable", installer, null, ReleaseNotesUrl: definition.OfficialHome))];
    }

    private static IReadOnlyList<DevEnvironmentVersion> ParsePython(DevEnvironmentDefinition definition, string _)
    {
        var versions = new[] { "3.13.1", "3.12.8", "3.11.11", "3.10.16" };
        return versions.Select(version =>
        {
            var installerName = $"python-{version}-amd64.exe";
            var installer = OfficialSource(
                $"https://www.python.org/ftp/python/{version}/{installerName}",
                $"Python {version}",
                installerName);
            var sourceName = $"Python-{version}.tgz";
            var source = OfficialSource(
                $"https://www.python.org/ftp/python/{version}/{sourceName}",
                $"Python {version} source",
                sourceName);
            return WithCandidates(definition.Id, new DevEnvironmentVersion(version, installer, source, ReleaseNotesUrl: definition.OfficialHome));
        }).ToArray();
    }

    private static IReadOnlyList<DevEnvironmentVersion> ParseAdoptium(DevEnvironmentDefinition definition, string json)
    {
        try
        {
            using var document = JsonDocument.Parse(json);
            var versions = new List<DevEnvironmentVersion>();
            foreach (var asset in document.RootElement.EnumerateArray().Take(12))
            {
                var version = asset.TryGetProperty("version", out var versionElement)
                    ? GetString(versionElement, "semver")
                    : string.Empty;
                if (!asset.TryGetProperty("binary", out var binary) ||
                    !binary.TryGetProperty("package", out var package))
                {
                    continue;
                }

                var link = GetString(package, "link");
                var name = Path.GetFileName(new Uri(link).LocalPath);
                if (string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(link) || string.IsNullOrWhiteSpace(name))
                {
                    continue;
                }

                var installer = OfficialSource(link, $"{definition.Name} {version}", name, Sha256: GetString(package, "checksum"), sizeBytes: GetLong(package, "size"));
                versions.Add(WithCandidates(definition.Id, new DevEnvironmentVersion(version, installer, null)));
            }

            return versions.Count > 0 ? versions : OfflineFallback(definition);
        }
        catch
        {
            return OfflineFallback(definition);
        }
    }

    private static IReadOnlyList<DevEnvironmentVersion> OfflineFallback(DevEnvironmentDefinition definition)
    {
        var source = new DownloadSource(definition.OfficialHome, $"{definition.Name} official downloads", $"{definition.Id}-official-download.url", "Manual", SourceLabel: "Manual");
        return [WithCandidates(definition.Id, new DevEnvironmentVersion("Latest", source, null, ReleaseNotesUrl: definition.OfficialHome))];
    }

    private static DevEnvironmentBuildRecipe BuildRecipe(DevEnvironmentDefinition definition)
    {
        return definition.Id switch
        {
            "go" => new(definition.Id, "Build Go from source after installing a bootstrap Go toolchain.", ["Go bootstrap toolchain", "Git", "Visual Studio Build Tools"], ["tar -xf <archive>", "cd go\\src", "make.bat"]),
            "python" => new(definition.Id, "Build CPython from source.", ["Visual Studio Build Tools", "Python dependencies"], ["tar -xf <archive>", "cd Python-*", "PCbuild\\build.bat -p x64"]),
            "java" => new(definition.Id, "Build OpenJDK/Temurin from source.", ["Boot JDK", "Visual Studio Build Tools", "Cygwin or MSYS2 where required"], ["tar -xf <archive>", "bash configure", "make images"]),
            "node" => new(definition.Id, "Build Node.js from source.", ["Python", "Visual Studio Build Tools", "Git"], ["tar -xf <archive>", "cd node-*", "vcbuild.bat release x64"]),
            "mysql" => new(definition.Id, "Build MySQL from source.", ["CMake", "Visual Studio Build Tools", "Bison", "Boost"], ["tar -xf <archive>", "cmake -S . -B build -G \"Visual Studio 17 2022\" -A x64", "cmake --build build --config Release"]),
            "cmake" => new(definition.Id, "Build CMake from source.", ["Visual Studio Build Tools"], ["tar -xf <archive>", "cmake -S . -B build -A x64", "cmake --build build --config Release"]),
            "docker" => new(definition.Id, "Docker Desktop source build is not exposed in this tool.", [], [], IsSupported: false),
            _ => new(definition.Id, "Download the source archive, verify prerequisites from the official documentation, then run the vendor build commands.", [], ["tar -xf <archive>"])
        };
    }

    private static DevEnvironmentVersion WithCandidates(string environmentId, DevEnvironmentVersion version)
    {
        var candidates = new List<DevEnvironmentDownloadCandidate>();
        if (version.Installer is not null)
        {
            candidates.Add(new DevEnvironmentDownloadCandidate(
                "installer-official",
                version.Installer.DisplayName,
                version.Installer,
                DevEnvironmentInstallMode.QuickInstall,
                version.Installer.SourceKind,
                version.Installer.MirrorRegion,
                true));
        }
        if (version.SourceArchive is not null)
        {
            candidates.Add(new DevEnvironmentDownloadCandidate(
                "source-official",
                version.SourceArchive.DisplayName,
                version.SourceArchive,
                DevEnvironmentInstallMode.SourceBuild,
                version.SourceArchive.SourceKind,
                version.SourceArchive.MirrorRegion));
        }

        foreach (var mirror in MirrorCandidates(environmentId, version))
        {
            if (candidates.All(item => !string.Equals(item.Source.Url, mirror.Source.Url, StringComparison.OrdinalIgnoreCase)))
            {
                candidates.Add(mirror);
            }
        }

        return version with { Candidates = candidates };
    }

    private static IEnumerable<DevEnvironmentDownloadCandidate> MirrorCandidates(string environmentId, DevEnvironmentVersion version)
    {
        var v = version.Version.TrimStart('v');
        if (string.IsNullOrWhiteSpace(v))
        {
            yield break;
        }

        if (environmentId == "node")
        {
            var installerName = $"node-v{v}-x64.msi";
            yield return Mirror("mirror-cn", "Node.js CN mirror", $"https://npmmirror.com/mirrors/node/v{v}/{installerName}", installerName, DevEnvironmentInstallMode.QuickInstall);
            var sourceName = $"node-v{v}.tar.gz";
            yield return Mirror("mirror-cn-source", "Node.js CN source mirror", $"https://npmmirror.com/mirrors/node/v{v}/{sourceName}", sourceName, DevEnvironmentInstallMode.SourceBuild);
        }
        else if (environmentId == "go")
        {
            var installerName = $"go{v}.windows-amd64.msi";
            yield return Mirror("mirror-cn", "Go CN mirror", $"https://mirrors.aliyun.com/golang/{installerName}", installerName, DevEnvironmentInstallMode.QuickInstall);
            var sourceName = $"go{v}.src.tar.gz";
            yield return Mirror("mirror-cn-source", "Go CN source mirror", $"https://mirrors.aliyun.com/golang/{sourceName}", sourceName, DevEnvironmentInstallMode.SourceBuild);
        }
        else if (environmentId == "python")
        {
            var installerName = $"python-{v}-amd64.exe";
            yield return Mirror("mirror-cn", "Python CN mirror", $"https://mirrors.huaweicloud.com/python/{v}/{installerName}", installerName, DevEnvironmentInstallMode.QuickInstall);
        }
    }

    private static DevEnvironmentDownloadCandidate Mirror(string id, string displayName, string url, string fileName, DevEnvironmentInstallMode mode)
    {
        return new DevEnvironmentDownloadCandidate(
            id,
            displayName,
            new DownloadSource(url, displayName, fileName, "MirrorCN", SourceLabel: "MirrorCN", MirrorRegion: "CN"),
            mode,
            "MirrorCN",
            "CN");
    }

    private static DownloadSource OfficialSource(string url, string displayName, string fileName, string? Sha256 = null, long? sizeBytes = null)
    {
        return new DownloadSource(url, displayName, fileName, "Official", Sha256, "Official", SizeBytes: sizeBytes);
    }

    private static string GetString(JsonElement element, string name)
    {
        return element.ValueKind == JsonValueKind.Object &&
            element.TryGetProperty(name, out var value)
            ? value.ValueKind == JsonValueKind.String ? value.GetString() ?? string.Empty : value.ToString()
            : string.Empty;
    }

    private static long? GetLong(JsonElement element, string name)
    {
        if (element.ValueKind != JsonValueKind.Object ||
            !element.TryGetProperty(name, out var value))
        {
            return null;
        }

        if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var number))
        {
            return number;
        }

        return long.TryParse(value.ToString(), out number) ? number : null;
    }

    private static DateTimeOffset? TryDate(string value)
    {
        return DateTimeOffset.TryParse(value, out var parsed) ? parsed : null;
    }
}
