using System.IO.Compression;
using System.Runtime.InteropServices;

namespace Flo.Installer.Commands;

internal static class InstallCommand
{
    public static async Task<int> ExecuteAsync(string manifestUrl, string installPath, CancellationToken ct = default)
    {
        if (TrustedPublicKey.IsPlaceholder)
        {
            Console.Error.WriteLine(
                "Trusted public key is unset. Bake the release public key into TrustedPublicKey.Bytes before running against a real manifest.");
            return 2;
        }

        string rid = DetectRid();
        Console.WriteLine($"Platform:     {rid}");
        Console.WriteLine($"Manifest:     {manifestUrl}");
        Console.WriteLine($"Install path: {installPath}");
        Console.WriteLine();

        using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
        var manifestClient = new ManifestClient(http);
        var downloader = new ArtifactDownloader(http);

        Manifest manifest;
        try
        {
            manifest = await manifestClient.DownloadAndVerifyAsync(new Uri(manifestUrl), TrustedPublicKey.Bytes, ct);
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"Manifest load failed: {ex.Message}");
            return 3;
        }

        Console.WriteLine($"Verified manifest: {manifest.Product} {manifest.Version} ({manifest.Channel})");
        Console.WriteLine();

        Directory.CreateDirectory(installPath);
        string cacheDir = Path.Combine(installPath, ".cache");
        Directory.CreateDirectory(cacheDir);

        foreach (var artifact in manifest.Artifacts)
        {
            if (!artifact.Platforms.TryGetValue(rid, out var platform))
            {
                Console.WriteLine($"[skip] {artifact.Id}: no entry for {rid}.");
                continue;
            }

            string cacheFile = Path.Combine(cacheDir, $"{artifact.Id}-{artifact.Version}-{rid}.bin");
            var progress = new Progress<DownloadProgress>(p =>
                Console.Write($"\r[get ] {artifact.Id} {artifact.Version}  {p.PercentComplete,6:0.0}%  "));

            await downloader.DownloadAsync(
                new Uri(platform.Url), cacheFile, platform.Sha256, platform.SizeBytes, progress, ct);
            Console.WriteLine();

            string target = Path.Combine(installPath, platform.InstallPath);
            Console.WriteLine($"[inst] {artifact.Id} -> {target}");
            Install(cacheFile, target, platform.Archive);
        }

        Console.WriteLine();
        Console.WriteLine("Install complete.");
        return 0;
    }

    private static void Install(string source, string target, string archive)
    {
        switch (archive)
        {
            case "zip":
                Directory.CreateDirectory(target);
                ZipFile.ExtractToDirectory(source, target, overwriteFiles: true);
                break;
            case "tar.gz":
                Directory.CreateDirectory(target);
                using (var fs = File.OpenRead(source))
                using (var gz = new GZipStream(fs, CompressionMode.Decompress))
                    System.Formats.Tar.TarFile.ExtractToDirectory(gz, target, overwriteFiles: true);
                break;
            case "none":
                Directory.CreateDirectory(Path.GetDirectoryName(target)!);
                File.Copy(source, target, overwrite: true);
                break;
            default:
                throw new InvalidOperationException($"Unknown archive type: {archive}");
        }
    }

    private static string DetectRid()
    {
        string os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win"
                  : RuntimeInformation.IsOSPlatform(OSPlatform.Linux)   ? "linux"
                  : throw new PlatformNotSupportedException("Only Windows and Linux are supported.");
        string arch = RuntimeInformation.ProcessArchitecture switch
        {
            Architecture.X64   => "x64",
            Architecture.Arm64 => "arm64",
            var a => throw new PlatformNotSupportedException($"Unsupported architecture: {a}.")
        };
        return $"{os}-{arch}";
    }
}
