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

namespace PSLauncher.Core.DocTool;

/// <summary>
/// Implémentation : copy récursif + atomic rename + backups horodatés.
/// Strictement parallèle à <see cref="ReportToolDeployer"/> mais avec
/// <c>_doc/</c> comme source au lieu de <c>_report/</c>. Voir le commentaire
/// de cette dernière classe pour les détails du mécanisme.
/// </summary>
public sealed class DocToolDeployer : IDocToolDeployer
{
    private const string SourceSubdir = "_doc";
    private const string BackupSuffix = ".backup-";
    private const string TimestampFormat = "yyyyMMdd-HHmmss";

    private readonly Func<DocToolConfig> _configProvider;
    private readonly ILogger<DocToolDeployer> _logger;

    public DocToolDeployer(
        Func<DocToolConfig> configProvider,
        ILogger<DocToolDeployer> 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 doc 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
        {
            if (Directory.Exists(stagingTarget)) Directory.Delete(stagingTarget, recursive: true);

            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);

            if (Directory.Exists(target))
                Directory.Move(target, backupTarget);
            Directory.Move(stagingTarget, target);

            PruneOldBackups(cfg);

            _logger.LogInformation("Doc 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 doc 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;
            }
            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));
        }
        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.");
        }

        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);

            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 doc 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, "Doc revert to {Backup} failed", backupFolderName);
            return new DeployResult(DeployStatus.Failed, 0, 0, target, ex.Message);
        }
    }

    private void PruneOldBackups(DocToolConfig 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) })
                .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 doc backup {Dir}", dirs[i].Name);
                }
                catch (Exception ex)
                {
                    _logger.LogDebug(ex, "Could not prune doc backup {Dir} (will retry next deploy)", dirs[i].Name);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Doc backup pruning skipped");
        }
    }
}
