using System.IO.Compression;
using Microsoft.Extensions.Logging;

namespace PSLauncher.Core.Installations;

public sealed class ZipInstaller : IZipInstaller
{
    private readonly ILogger<ZipInstaller> _logger;

    public ZipInstaller(ILogger<ZipInstaller> logger) => _logger = logger;

    public async Task<string> InstallAsync(
        string zipPath,
        string targetFolder,
        IProgress<InstallProgress>? progress,
        CancellationToken ct)
    {
        if (!File.Exists(zipPath))
            throw new FileNotFoundException("ZIP not found", zipPath);

        var stagingFolder = targetFolder + ".tmp";
        if (Directory.Exists(stagingFolder)) Directory.Delete(stagingFolder, recursive: true);
        Directory.CreateDirectory(stagingFolder);

        try
        {
            await Task.Run(() => ExtractZip(zipPath, stagingFolder, progress, ct), ct).ConfigureAwait(false);

            if (Directory.Exists(targetFolder))
                throw new InvalidOperationException($"Le dossier cible existe déjà : {targetFolder}");

            Directory.Move(stagingFolder, targetFolder);
            _logger.LogInformation("Installed to {Target}", targetFolder);
            return targetFolder;
        }
        catch
        {
            // Nettoie le staging si échec
            try { if (Directory.Exists(stagingFolder)) Directory.Delete(stagingFolder, recursive: true); } catch { }
            throw;
        }
    }

    private static void ExtractZip(string zipPath, string stagingFolder, IProgress<InstallProgress>? progress, CancellationToken ct)
    {
        using var archive = ZipFile.OpenRead(zipPath);
        long totalBytes = 0;
        foreach (var e in archive.Entries) totalBytes += e.Length;
        long total = archive.Entries.Count;

        long doneEntries = 0;
        long doneBytes = 0;

        // Détecte si toutes les entrées partagent un préfixe racine commun (ex: "Proserve v1.4.6/")
        // → on strip ce préfixe pour aplatir l'arbo dans stagingFolder.
        var rootPrefix = DetectCommonRootPrefix(archive);

        foreach (var entry in archive.Entries)
        {
            ct.ThrowIfCancellationRequested();
            var relative = entry.FullName;
            if (rootPrefix is not null && relative.StartsWith(rootPrefix, StringComparison.Ordinal))
                relative = relative[rootPrefix.Length..];

            if (string.IsNullOrEmpty(relative)) { doneEntries++; continue; }

            var target = Path.GetFullPath(Path.Combine(stagingFolder, relative));
            if (!target.StartsWith(Path.GetFullPath(stagingFolder), StringComparison.OrdinalIgnoreCase))
                throw new InvalidDataException($"Entrée ZIP suspecte (zip-slip) : {entry.FullName}");

            if (relative.EndsWith('/') || relative.EndsWith('\\'))
            {
                Directory.CreateDirectory(target);
            }
            else
            {
                Directory.CreateDirectory(Path.GetDirectoryName(target)!);
                entry.ExtractToFile(target, overwrite: true);
                doneBytes += entry.Length;
            }
            doneEntries++;
            progress?.Report(new InstallProgress(doneEntries, total, doneBytes, totalBytes, relative));
        }
    }

    private static string? DetectCommonRootPrefix(ZipArchive archive)
    {
        string? prefix = null;
        foreach (var e in archive.Entries)
        {
            var slash = e.FullName.IndexOf('/');
            if (slash <= 0) return null;
            var first = e.FullName[..(slash + 1)];
            if (prefix is null) prefix = first;
            else if (prefix != first) return null;
        }
        return prefix;
    }
}
