using System.Globalization;
using Microsoft.Extensions.Logging;
using PSLauncher.Models;

namespace PSLauncher.Core.ReportTool;

/// <summary>
/// Implémentation : copy récursif + atomic rename + backups horodatés.
/// L'atomicité du rename garantit qu'Apache (qui sert les fichiers en parallèle)
/// ne tombera jamais sur un état mi-déployé. Au pire il sert l'ancienne version
/// pendant 1ms le temps du rename, puis sert la nouvelle.
///
/// Backups : à chaque deploy, l'ancien dossier est conservé sous le nom
/// <c>{FolderName}.backup-yyyyMMdd-HHmmss</c>. On garde les <c>MaxBackups</c>
/// plus récents et on supprime les autres en best-effort. Permet le revert
/// manuel via Settings → Avancés → Outil Report.
/// </summary>
public sealed class ReportToolDeployer : IReportToolDeployer
{
    private const string SourceSubdir = "_report";
    private const string BackupSuffix = ".backup-";
    // Format trié-par-nom = trié-par-date (lexicographique == chronologique)
    private const string TimestampFormat = "yyyyMMdd-HHmmss";

    private readonly Func<ReportToolConfig> _configProvider;
    private readonly ILogger<ReportToolDeployer> _logger;

    public ReportToolDeployer(
        Func<ReportToolConfig> configProvider,
        ILogger<ReportToolDeployer> logger)
    {
        _configProvider = configProvider;
        _logger = logger;
    }

    public async Task<DeployResult> DeployAsync(
        string installFolder,
        IProgress<DeployProgress>? progress,
        CancellationToken ct)
    {
        var cfg = _configProvider();
        var source = Path.Combine(installFolder, SourceSubdir);
        var target = Path.Combine(cfg.HtdocsRoot, cfg.FolderName);

        if (!Directory.Exists(source))
        {
            _logger.LogInformation("No {Sub}/ directory in {Folder}, skipping report deploy", SourceSubdir, installFolder);
            return new DeployResult(DeployStatus.SkippedNoSourceFolder, 0, 0, target);
        }

        if (!Directory.Exists(cfg.HtdocsRoot))
        {
            _logger.LogWarning("HtdocsRoot {Root} not found — XAMPP not installed at expected location", cfg.HtdocsRoot);
            return new DeployResult(DeployStatus.XamppNotFound, 0, 0, target,
                $"Le dossier {cfg.HtdocsRoot} est introuvable. XAMPP est-il installé à cet emplacement ? (Settings → Avancés)");
        }

        var stagingTarget = target + ".new";
        var backupTarget = target + BackupSuffix + DateTime.UtcNow.ToString(TimestampFormat, CultureInfo.InvariantCulture);

        try
        {
            // Nettoyage d'un éventuel résidu de tentative précédente (le .new orphelin)
            if (Directory.Exists(stagingTarget)) Directory.Delete(stagingTarget, recursive: true);

            // Copy avec progress
            var allFiles = Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories).ToList();
            int total = allFiles.Count;
            long bytesTotal = 0;

            await Task.Run(() =>
            {
                Directory.CreateDirectory(stagingTarget);
                int done = 0;
                foreach (var srcFile in allFiles)
                {
                    ct.ThrowIfCancellationRequested();
                    var rel = Path.GetRelativePath(source, srcFile);
                    var dstFile = Path.Combine(stagingTarget, rel);
                    var dstDir = Path.GetDirectoryName(dstFile);
                    if (!string.IsNullOrEmpty(dstDir) && !Directory.Exists(dstDir))
                        Directory.CreateDirectory(dstDir);
                    File.Copy(srcFile, dstFile, overwrite: true);
                    bytesTotal += new FileInfo(srcFile).Length;
                    done++;
                    progress?.Report(new DeployProgress(done, total, rel));
                }
            }, ct).ConfigureAwait(false);

            // Atomic swap : current → backup-{ts}, .new → current
            if (Directory.Exists(target))
            {
                Directory.Move(target, backupTarget);
            }
            Directory.Move(stagingTarget, target);

            // Prune des vieux backups (best-effort, ne bloque pas le retour)
            PruneOldBackups(cfg);

            _logger.LogInformation("Report tool deployed to {Target} ({Files} files, {Bytes} bytes), backup at {Backup}",
                target, total, bytesTotal, backupTarget);
            return new DeployResult(DeployStatus.Deployed, total, bytesTotal, target);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to deploy report tool to {Target}", target);
            try { if (Directory.Exists(stagingTarget)) Directory.Delete(stagingTarget, recursive: true); }
            catch { /* ignore */ }
            return new DeployResult(DeployStatus.Failed, 0, 0, target, ex.Message);
        }
    }

    public Task<IReadOnlyList<BackupInfo>> ListBackupsAsync(CancellationToken ct)
    {
        var cfg = _configProvider();
        var prefix = cfg.FolderName + BackupSuffix;

        if (!Directory.Exists(cfg.HtdocsRoot))
            return Task.FromResult<IReadOnlyList<BackupInfo>>(Array.Empty<BackupInfo>());

        var list = new List<BackupInfo>();
        foreach (var dir in Directory.EnumerateDirectories(cfg.HtdocsRoot, prefix + "*", SearchOption.TopDirectoryOnly))
        {
            ct.ThrowIfCancellationRequested();
            var name = Path.GetFileName(dir);
            var tsPart = name.Substring(prefix.Length);
            if (!DateTime.TryParseExact(tsPart, TimestampFormat, CultureInfo.InvariantCulture,
                    DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ts))
            {
                continue; // dossier non conforme, on ignore
            }
            long size = 0;
            try
            {
                foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
                    size += new FileInfo(f).Length;
            }
            catch { /* size best-effort */ }
            list.Add(new BackupInfo(name, ts, size));
        }
        // Plus récent en premier
        list.Sort((a, b) => b.TimestampUtc.CompareTo(a.TimestampUtc));
        return Task.FromResult<IReadOnlyList<BackupInfo>>(list);
    }

    public async Task<DeployResult> RevertAsync(string backupFolderName, CancellationToken ct)
    {
        var cfg = _configProvider();
        var backupPath = Path.Combine(cfg.HtdocsRoot, backupFolderName);
        var target = Path.Combine(cfg.HtdocsRoot, cfg.FolderName);

        if (!Directory.Exists(backupPath))
        {
            return new DeployResult(DeployStatus.BackupNotFound, 0, 0, target,
                $"Le backup {backupFolderName} n'existe plus.");
        }

        // Le revert lui-même crée un backup de l'état actuel, pour qu'on puisse
        // revert le revert si besoin.
        var newBackupForCurrent = target + BackupSuffix + DateTime.UtcNow.ToString(TimestampFormat, CultureInfo.InvariantCulture);

        try
        {
            await Task.Run(() =>
            {
                ct.ThrowIfCancellationRequested();
                if (Directory.Exists(target))
                    Directory.Move(target, newBackupForCurrent);
                Directory.Move(backupPath, target);
            }, ct).ConfigureAwait(false);

            // Compte les fichiers du backup restauré pour le retour
            int files = 0;
            long bytes = 0;
            try
            {
                foreach (var f in Directory.EnumerateFiles(target, "*", SearchOption.AllDirectories))
                {
                    files++;
                    bytes += new FileInfo(f).Length;
                }
            }
            catch { /* best-effort */ }

            PruneOldBackups(cfg);
            _logger.LogInformation("Reverted to backup {Backup} ({Files} files), previous current saved as {New}",
                backupFolderName, files, newBackupForCurrent);
            return new DeployResult(DeployStatus.Deployed, files, bytes, target);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Revert to {Backup} failed", backupFolderName);
            return new DeployResult(DeployStatus.Failed, 0, 0, target, ex.Message);
        }
    }

    /// <summary>
    /// Garde les <c>MaxBackups</c> plus récents, supprime les autres en best-effort.
    /// Si <c>MaxBackups = 0</c>, supprime tous les backups (mode "no history").
    /// </summary>
    private void PruneOldBackups(ReportToolConfig cfg)
    {
        try
        {
            var prefix = cfg.FolderName + BackupSuffix;
            var dirs = Directory.EnumerateDirectories(cfg.HtdocsRoot, prefix + "*", SearchOption.TopDirectoryOnly)
                .Select(d => new { Path = d, Name = Path.GetFileName(d) })
                // Tri descending par nom = descending par date (yyyyMMdd-HHmmss est lexicographique)
                .OrderByDescending(d => d.Name, StringComparer.Ordinal)
                .ToList();

            int keep = Math.Max(0, cfg.MaxBackups);
            for (int i = keep; i < dirs.Count; i++)
            {
                try
                {
                    Directory.Delete(dirs[i].Path, recursive: true);
                    _logger.LogDebug("Pruned old report backup {Dir}", dirs[i].Name);
                }
                catch (Exception ex)
                {
                    // Apache peut tenir un handle sur un fichier (logs ouverts, etc.).
                    // On retentera au prochain deploy. Pas bloquant.
                    _logger.LogDebug(ex, "Could not prune backup {Dir} (will retry next deploy)", dirs[i].Name);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Backup pruning skipped");
        }
    }
}
