using System.IO;
using System.IO.Compression;
using ClaudeDo.Installer.Core;
using ClaudeDo.Releases;

namespace ClaudeDo.Installer.Steps;

public sealed class DownloadAndExtractStep : IInstallStep
{
    private readonly IReleaseClient _releases;

    public DownloadAndExtractStep(IReleaseClient releases)
    {
        _releases = releases;
    }

    public string Name => "Download and Extract";

    public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(ctx.InstallDirectory))
            return StepResult.Fail("Install directory is not set.");

        progress.Report("Fetching latest release metadata...");
        var release = await _releases.GetLatestReleaseAsync(ct);
        if (release is null)
            return StepResult.Fail("Could not reach the release server. Check your network connection and try again.");

        var zipAsset = release.Assets.FirstOrDefault(a =>
            a.Name.StartsWith("ClaudeDo-", StringComparison.OrdinalIgnoreCase) &&
            a.Name.EndsWith("-win-x64.zip", StringComparison.OrdinalIgnoreCase));
        var checksumAsset = release.Assets.FirstOrDefault(a =>
            a.Name.Equals("checksums.txt", StringComparison.OrdinalIgnoreCase));

        if (zipAsset is null)
            return StepResult.Fail("Release zip asset not found in release metadata.");
        if (checksumAsset is null)
            return StepResult.Fail("checksums.txt not found in release metadata.");

        var scratchDir = Path.Combine(Path.GetTempPath(), "ClaudeDo-install-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(scratchDir);

        try
        {
            var zipPath = Path.Combine(scratchDir, zipAsset.Name);
            var checksumPath = Path.Combine(scratchDir, "checksums.txt");

            var totalMb = zipAsset.Size / (1024 * 1024);
            progress.Report($"Downloading {zipAsset.Name} ({totalMb} MB)...");
            long lastReportedMb = -1;
            await _releases.DownloadAsync(zipAsset.BrowserDownloadUrl, zipPath,
                new Progress<long>(b =>
                {
                    var mb = b / (1024 * 1024);
                    if (mb == lastReportedMb) return;
                    lastReportedMb = mb;
                    // Leading "\r" tells the UI to overwrite the previous line instead of appending.
                    progress.Report($"\r  {mb} / {totalMb} MB downloaded");
                }),
                ct);

            progress.Report("Downloading checksums...");
            await _releases.DownloadAsync(checksumAsset.BrowserDownloadUrl, checksumPath,
                new Progress<long>(_ => { }), ct);

            progress.Report("Verifying checksum...");
            var map = ChecksumVerifier.ParseChecksumsFile(await File.ReadAllTextAsync(checksumPath, ct));
            if (!map.TryGetValue(zipAsset.Name, out var expectedHash))
                return StepResult.Fail($"No checksum entry for {zipAsset.Name} in checksums.txt.");
            if (!ChecksumVerifier.Verify(zipPath, expectedHash))
                return StepResult.Fail("Checksum mismatch — the downloaded zip may be corrupt or tampered with.");

            // Only after verification do we touch the install directory.
            progress.Report("Stashing previous app/worker binaries...");
            var appDest = Path.Combine(ctx.InstallDirectory, "app");
            var workerDest = Path.Combine(ctx.InstallDirectory, "worker");
            var appBak = appDest + ".bak";
            var workerBak = workerDest + ".bak";

            if (Directory.Exists(appBak))    Directory.Delete(appBak, recursive: true);
            if (Directory.Exists(workerBak)) Directory.Delete(workerBak, recursive: true);
            if (Directory.Exists(appDest))   Directory.Move(appDest, appBak);
            if (Directory.Exists(workerDest)) Directory.Move(workerDest, workerBak);

            progress.Report("Extracting...");
            Directory.CreateDirectory(ctx.InstallDirectory);
            try
            {
                ZipFile.ExtractToDirectory(zipPath, ctx.InstallDirectory, overwriteFiles: true);
            }
            catch (Exception ex)
            {
                // Roll back to previous binaries.
                if (Directory.Exists(appDest))   Directory.Delete(appDest, recursive: true);
                if (Directory.Exists(workerDest)) Directory.Delete(workerDest, recursive: true);
                if (Directory.Exists(appBak))    Directory.Move(appBak, appDest);
                if (Directory.Exists(workerBak)) Directory.Move(workerBak, workerDest);
                return StepResult.Fail(
                    $"Extraction failed; previous binaries have been restored: {ex.Message}.");
            }

            // Success — drop stash.
            if (Directory.Exists(appBak))    Directory.Delete(appBak, recursive: true);
            if (Directory.Exists(workerBak)) Directory.Delete(workerBak, recursive: true);

            ctx.InstalledVersion = release.TagName.TrimStart('v', 'V');
            return StepResult.Ok();
        }
        finally
        {
            try { Directory.Delete(scratchDir, recursive: true); } catch { /* best effort */ }
        }
    }
}
