using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using PSLauncher.App.Views;
using PSLauncher.Core.Configuration;
using PSLauncher.Core.DocTool;
using PSLauncher.Core.Downloads;
using PSLauncher.Core.Installations;
using PSLauncher.Core.Licensing;
using PSLauncher.Core.Localization;
using PSLauncher.Core.Migrations;
using PSLauncher.Core.ReportTool;
using PSLauncher.Models;

namespace PSLauncher.App.ViewModels;

public sealed record LanguageOption(string Code, string Name)
{
    // Override pour que la SelectionBox d'un ComboBox affiche le libellé clair
    // (sinon WPF affiche la forme record "LanguageOption { Code = ..., Name = ... }").
    public override string ToString() => Name;
}

public sealed partial class SettingsViewModel : ObservableObject
{
    private readonly IConfigStore _configStore;
    private readonly LocalConfig _config;
    private readonly ILicenseService _licenseService;
    private readonly IDownloadStateStore _downloadStore;
    private readonly IDatabaseMigrationService _migrationService;
    private readonly IReportToolDeployer _reportDeployer;
    private readonly IDocToolDeployer _docDeployer;
    private readonly IInstallationRegistry _registry;
    private readonly HttpClient _http;
    private readonly ILogger<SettingsViewModel> _logger;

    public string LauncherVersion { get; }
    public string MachineId { get; }
    public string LogsDirectory { get; }
    public string CacheDirectory { get; }

    [ObservableProperty] private string _serverBaseUrl;
    [ObservableProperty] private string _installRoot;
    [ObservableProperty] private string _language;
    [ObservableProperty] private string _reportUrl;
    [ObservableProperty] private string _documentationUrl;

    // ----- DB Migration settings -----
    [ObservableProperty] private string _dbHost;
    [ObservableProperty] private uint _dbPort;
    [ObservableProperty] private string _dbUser;
    [ObservableProperty] private string _dbPassword;
    [ObservableProperty] private string _dbName;
    [ObservableProperty] private bool _dbAutoApplyMigrations;
    [ObservableProperty] private string? _dbConnectionStatus;
    [ObservableProperty] private bool _isTestingDb;
    [ObservableProperty] private string? _migrationsReplayStatus;
    [ObservableProperty] private bool _isReplayingMigrations;

    // ----- Report Tool deploy settings -----
    [ObservableProperty] private string _reportHtdocsRoot;
    [ObservableProperty] private string _reportFolderName;
    [ObservableProperty] private bool _reportAutoDeploy;
    [ObservableProperty] private int _reportMaxBackups;
    [ObservableProperty] private string? _reportDeployStatus;
    [ObservableProperty] private bool _isDeployingReport;
    public System.Collections.ObjectModel.ObservableCollection<ReportBackupViewModel> ReportBackups { get; }
        = new();
    public bool HasReportBackups => ReportBackups.Count > 0;

    // ----- Doc Tool deploy settings (mirror of Report) -----
    [ObservableProperty] private string _docHtdocsRoot;
    [ObservableProperty] private string _docFolderName;
    [ObservableProperty] private bool _docAutoDeploy;
    [ObservableProperty] private int _docMaxBackups;
    [ObservableProperty] private string? _docDeployStatus;
    [ObservableProperty] private bool _isDeployingDoc;
    public System.Collections.ObjectModel.ObservableCollection<DocBackupViewModel> DocBackups { get; }
        = new();
    public bool HasDocBackups => DocBackups.Count > 0;

    public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
        PSLauncher.Core.Localization.Strings.Available
            .Select(t => new LanguageOption(t.Code, t.Name))
            .ToList();
    [ObservableProperty] private string? _connectionStatus;
    [ObservableProperty] private bool _isTestingConnection;
    [ObservableProperty] private string _cacheSizeDisplay = "—";

    public string LicenseInfo
    {
        get
        {
            var l = _licenseService.GetCached();
            if (l is null) return Strings.LicenseSummaryNone;
            if (l.Status == "valid")
                return Strings.LicenseSummaryValid(l.OwnerName ?? "—",
                    Strings.FormatDate(l.DownloadEntitlementUntil));
            if (l.Status == "expired")
                return Strings.LicenseSummaryExpired(Strings.FormatDate(l.DownloadEntitlementUntil));
            return $"{l.OwnerName} • {l.Status}";
        }
    }

    public string LicenseIcon
    {
        get
        {
            var l = _licenseService.GetCached();
            if (l is null) return "🔒";
            return l.Status switch
            {
                "valid" when IsExpiringSoon(l) => "⏰",
                "valid"                         => "✓",
                "expired"                       => "⚠",
                "revoked"                       => "🚫",
                _                                => "❓",
            };
        }
    }

    public string LicenseSeverity
    {
        get
        {
            var l = _licenseService.GetCached();
            if (l is null) return "Error";
            return l.Status switch
            {
                "valid" when IsExpiringSoon(l) => "Warning",
                "valid"                         => "Ok",
                _                                => "Error",
            };
        }
    }

    private static bool IsExpiringSoon(LicenseValidationResponse l)
    {
        if (l.DownloadEntitlementUntil is not { } until) return false;
        return (until - DateTime.UtcNow).TotalDays < 30;
    }

    public SettingsViewModel(
        IConfigStore configStore,
        LocalConfig config,
        ILicenseService licenseService,
        IDownloadStateStore downloadStore,
        IDatabaseMigrationService migrationService,
        IReportToolDeployer reportDeployer,
        IDocToolDeployer docDeployer,
        IInstallationRegistry registry,
        HttpClient http,
        ILogger<SettingsViewModel> logger)
    {
        _configStore = configStore;
        _config = config;
        _licenseService = licenseService;
        _downloadStore = downloadStore;
        _migrationService = migrationService;
        _reportDeployer = reportDeployer;
        _docDeployer = docDeployer;
        _registry = registry;
        _http = http;
        _logger = logger;

        _serverBaseUrl = config.ServerBaseUrl;
        _installRoot = config.InstallRoot;
        _language = string.IsNullOrWhiteSpace(config.Language) ? "auto" : config.Language;
        _reportUrl = config.ReportUrl;
        _documentationUrl = config.DocumentationUrl;

        _dbHost = config.Database.Host;
        _dbPort = config.Database.Port;
        _dbUser = config.Database.User;
        _dbPassword = DatabasePasswordProtector.Unprotect(config.Database.EncryptedPassword) ?? string.Empty;
        _dbName = config.Database.Database;
        _dbAutoApplyMigrations = config.Database.AutoApplyMigrations;

        _reportHtdocsRoot = config.ReportTool.HtdocsRoot;
        _reportFolderName = config.ReportTool.FolderName;
        _reportAutoDeploy = config.ReportTool.AutoDeploy;
        _reportMaxBackups = config.ReportTool.MaxBackups;

        _docHtdocsRoot = config.DocTool.HtdocsRoot;
        _docFolderName = config.DocTool.FolderName;
        _docAutoDeploy = config.DocTool.AutoDeploy;
        _docMaxBackups = config.DocTool.MaxBackups;

        // Charge les listes de backups en arrière-plan (UI immédiate puis remplies quand prêtes)
        _ = LoadReportBackupsAsync();
        _ = LoadDocBackupsAsync();

        LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
        MachineId = licenseService.GetMachineId();
        LogsDirectory = App.LogsDirectory;
        CacheDirectory = downloadStore.GetDownloadsDirectory();
        RefreshCacheSize();
    }

    [RelayCommand]
    private async Task TestConnectionAsync()
    {
        ConnectionStatus = "Test en cours…";
        IsTestingConnection = true;
        try
        {
            var url = ServerBaseUrl.TrimEnd('/') + "/health";
            using var resp = await _http.GetAsync(url);
            if (resp.IsSuccessStatusCode)
            {
                var body = await resp.Content.ReadAsStringAsync();
                ConnectionStatus = $"✓ OK ({(int)resp.StatusCode}) — {body.Substring(0, Math.Min(80, body.Length))}";
            }
            else
            {
                ConnectionStatus = $"✗ HTTP {(int)resp.StatusCode}";
            }
        }
        catch (Exception ex)
        {
            ConnectionStatus = $"✗ Erreur : {ex.Message}";
        }
        finally
        {
            IsTestingConnection = false;
        }
    }

    [RelayCommand]
    private void BrowseInstallRoot()
    {
        var dlg = new Microsoft.Win32.OpenFolderDialog
        {
            Title = Strings.SettingsInstallRoot,
            InitialDirectory = Directory.Exists(InstallRoot) ? InstallRoot : Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
        };
        if (dlg.ShowDialog() == true)
        {
            InstallRoot = dlg.FolderName;
        }
    }

    [RelayCommand]
    private void OpenLogsFolder()
    {
        if (Directory.Exists(LogsDirectory))
            Process.Start(new ProcessStartInfo { FileName = LogsDirectory, UseShellExecute = true, Verb = "open" });
    }

    [RelayCommand]
    private void OpenCacheFolder()
    {
        if (Directory.Exists(CacheDirectory))
            Process.Start(new ProcessStartInfo { FileName = CacheDirectory, UseShellExecute = true, Verb = "open" });
    }

    [RelayCommand]
    private void ClearCache()
    {
        var confirm = ThemedMessageBox.Show(
            Strings.MsgClearCacheConfirm,
            Strings.MsgBoxConfirm,
            MessageBoxButton.YesNo, MessageBoxImage.Warning, MessageBoxResult.No);
        if (confirm != MessageBoxResult.Yes) return;

        try
        {
            foreach (var f in Directory.EnumerateFiles(CacheDirectory))
            {
                try { File.Delete(f); } catch (Exception ex) { _logger.LogWarning(ex, "Cannot delete {F}", f); }
            }
            RefreshCacheSize();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "ClearCache failed");
            ThemedMessageBox.Show(ex.Message, Strings.MsgBoxError, MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }

    [RelayCommand]
    private void CopyMachineId()
    {
        try { Clipboard.SetText(MachineId); }
        catch (Exception ex) { _logger.LogWarning(ex, "Clipboard failed"); }
    }

    [RelayCommand]
    private void Save()
    {
        var languageChanged = !string.Equals(
            (_config.Language ?? "auto").ToLowerInvariant(),
            (Language ?? "auto").ToLowerInvariant(),
            StringComparison.Ordinal);

        _config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/');
        _config.InstallRoot = InstallRoot;
        _config.Language = Language ?? "auto";
        _config.ReportUrl = ReportUrl?.Trim() ?? "";
        _config.DocumentationUrl = DocumentationUrl?.Trim() ?? "";

        // DB settings
        _config.Database.Host = DbHost.Trim();
        _config.Database.Port = DbPort > 0 ? DbPort : 3306;
        _config.Database.User = DbUser.Trim();
        _config.Database.EncryptedPassword = string.IsNullOrEmpty(DbPassword)
            ? null
            : DatabasePasswordProtector.Protect(DbPassword);
        _config.Database.Database = DbName.Trim();
        _config.Database.AutoApplyMigrations = DbAutoApplyMigrations;

        _config.ReportTool.HtdocsRoot = ReportHtdocsRoot.Trim();
        _config.ReportTool.FolderName = ReportFolderName.Trim();
        _config.ReportTool.AutoDeploy = ReportAutoDeploy;
        _config.ReportTool.MaxBackups = Math.Max(0, ReportMaxBackups);

        _config.DocTool.HtdocsRoot = DocHtdocsRoot.Trim();
        _config.DocTool.FolderName = DocFolderName.Trim();
        _config.DocTool.AutoDeploy = DocAutoDeploy;
        _config.DocTool.MaxBackups = Math.Max(0, DocMaxBackups);

        _configStore.Save(_config);
        _logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged);

        if (languageChanged)
        {
            // Le launcher doit être relancé pour que les bindings x:Static reflètent
            // la nouvelle langue. Plutôt que de demander à l'utilisateur de fermer/rouvrir,
            // on spawne un cmd qui attend la fermeture puis relance.
            var ok = ThemedMessageBox.Show(
                Strings.MsgLanguageRestart,
                Strings.MsgBoxLanguageChange,
                MessageBoxButton.OKCancel, MessageBoxImage.Information, MessageBoxResult.OK);
            if (ok == MessageBoxResult.OK)
            {
                try
                {
                    var exe = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
                    if (!string.IsNullOrEmpty(exe))
                    {
                        var pid = Environment.ProcessId;
                        var cmd = $"/c (echo Wait && timeout /t 1 /nobreak > nul) & start \"\" \"{exe}\"";
                        Process.Start(new ProcessStartInfo
                        {
                            FileName = "cmd.exe",
                            Arguments = cmd,
                            UseShellExecute = false,
                            CreateNoWindow = true,
                        });
                        Application.Current.Shutdown();
                    }
                }
                catch (Exception ex) { _logger.LogError(ex, "Restart on language change failed"); }
            }
        }
    }

    [RelayCommand]
    private void DeactivateLicense()
    {
        var confirm = ThemedMessageBox.Show(
            Strings.MsgDeactivateLicenseConfirm,
            Strings.MsgBoxConfirm,
            MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
        if (confirm != MessageBoxResult.Yes) return;
        _licenseService.Clear();
        OnPropertyChanged(nameof(LicenseInfo));
    }

    private void RefreshCacheSize()
    {
        try
        {
            long total = 0;
            if (Directory.Exists(CacheDirectory))
            {
                foreach (var f in Directory.EnumerateFiles(CacheDirectory))
                    total += new FileInfo(f).Length;
            }
            CacheSizeDisplay = FormatSize(total);
        }
        catch { CacheSizeDisplay = "—"; }
    }

    private static string FormatSize(long bytes) => Strings.FormatSize(bytes);

    /// <summary>
    /// Re-déploie l'outil Report depuis la dernière version PROSERVE installée
    /// vers htdocs. Utile si le déploiement post-install avait échoué (XAMPP
    /// déplacé, htdocs absent), ou pour resync après changement du chemin htdocs
    /// dans Settings.
    /// </summary>
    [RelayCommand]
    private async Task RedeployReportAsync()
    {
        var installed = _registry.Scan().OrderByDescending(v => v.Version).FirstOrDefault();
        if (installed is null)
        {
            ReportDeployStatus = "Aucune version installée — installe d'abord une version qui contient _report/.";
            return;
        }
        IsDeployingReport = true;
        ReportDeployStatus = $"Déploiement depuis v{installed.Version}…";
        try
        {
            // Persiste les éventuels changements de chemin avant le deploy
            Save();
            var progress = new Progress<DeployProgress>(dp =>
            {
                if (dp.FilesTotal > 0)
                    ReportDeployStatus = $"{dp.FilesDone}/{dp.FilesTotal} : {dp.CurrentFilename}";
            });
            var result = await _reportDeployer.DeployAsync(installed.FolderPath, progress, CancellationToken.None);
            ReportDeployStatus = result.Status switch
            {
                DeployStatus.Deployed              => $"✓ {result.FilesDeployed} fichiers déployés vers {result.TargetPath}",
                DeployStatus.SkippedNoSourceFolder => $"⚠ La version v{installed.Version} ne contient pas de _report/ — rien à déployer",
                DeployStatus.XamppNotFound         => $"✗ {result.ErrorMessage}",
                DeployStatus.Failed                => $"✗ {result.ErrorMessage}",
                _                                  => "?",
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Redeploy report failed");
            ReportDeployStatus = $"✗ {ex.Message}";
        }
        finally
        {
            IsDeployingReport = false;
        }
    }

    // ===================== DB MIGRATIONS =====================

    /// <summary>
    /// Test la connexion à la DB MySQL avec les paramètres en cours de saisie.
    /// Persiste temporairement les valeurs sur _config.Database avant le test
    /// pour que le service les voie via son configProvider, puis remet (et le
    /// Save() final écrasera quand l'utilisateur cliquera Enregistrer).
    /// </summary>
    [RelayCommand]
    private async Task TestDbAsync()
    {
        DbConnectionStatus = Strings.SettingsTest + "…";
        IsTestingDb = true;
        // On applique temporairement les valeurs saisies
        var oldHost = _config.Database.Host;
        var oldPort = _config.Database.Port;
        var oldUser = _config.Database.User;
        var oldPwd  = _config.Database.EncryptedPassword;
        var oldDb   = _config.Database.Database;
        try
        {
            _config.Database.Host = DbHost.Trim();
            _config.Database.Port = DbPort > 0 ? DbPort : 3306;
            _config.Database.User = DbUser.Trim();
            _config.Database.EncryptedPassword = string.IsNullOrEmpty(DbPassword)
                ? null
                : DatabasePasswordProtector.Protect(DbPassword);
            _config.Database.Database = DbName.Trim();

            var err = await _migrationService.TestConnectionAsync(CancellationToken.None);
            DbConnectionStatus = err is null ? "✓ OK" : $"✗ {err}";
        }
        catch (Exception ex) { DbConnectionStatus = $"✗ {ex.Message}"; }
        finally
        {
            // Restaure : seul Save() doit écrire pour de bon
            _config.Database.Host = oldHost;
            _config.Database.Port = oldPort;
            _config.Database.User = oldUser;
            _config.Database.EncryptedPassword = oldPwd;
            _config.Database.Database = oldDb;
            IsTestingDb = false;
        }
    }

    /// <summary>
    /// Re-joue les migrations bundled dans le _migrations/ de la version actuellement
    /// installée la plus récente. Utile si l'auto-apply post-install avait échoué (ex.
    /// XAMPP éteint à ce moment-là), ou pour rejouer manuellement après ajout d'une
    /// nouvelle migration côté dev.
    /// </summary>
    [RelayCommand]
    private async Task ReplayMigrationsAsync()
    {
        var installed = _registry.Scan().OrderByDescending(v => v.Version).FirstOrDefault();
        if (installed is null)
        {
            MigrationsReplayStatus = "Aucune version installée — installe d'abord une version avec _migrations/.";
            return;
        }
        IsReplayingMigrations = true;
        MigrationsReplayStatus = $"Réapplication des migrations pour v{installed.Version}…";
        try
        {
            var progress = new Progress<MigrationProgress>(mp =>
            {
                if (mp.Total > 0)
                    MigrationsReplayStatus = $"{mp.Done + 1}/{mp.Total} : {mp.CurrentFilename}";
            });
            // Il faut persister les éventuelles modifs DB avant le test (le service lit la config courante)
            Save();
            var result = await _migrationService.ApplyMigrationsAsync(
                installed.FolderPath, progress, CancellationToken.None);
            if (result.AllSucceeded)
            {
                MigrationsReplayStatus = result.AppliedCount > 0
                    ? $"✓ {result.AppliedCount} migration(s) appliquée(s), {result.SkippedCount} déjà en place."
                    : $"✓ Tout est à jour ({result.SkippedCount} déjà appliquée(s))";
            }
            else
            {
                MigrationsReplayStatus = $"✗ Échec : {result.FailureMessage}";
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Replay migrations failed");
            MigrationsReplayStatus = $"✗ Erreur : {ex.Message}";
        }
        finally
        {
            IsReplayingMigrations = false;
        }
    }

    // ===================== REPORT TOOL BACKUPS =====================

    private async Task LoadReportBackupsAsync()
    {
        try
        {
            var list = await _reportDeployer.ListBackupsAsync(CancellationToken.None);
            ReportBackups.Clear();
            foreach (var b in list)
                ReportBackups.Add(new ReportBackupViewModel(b, RevertReportToBackupAsync));
            OnPropertyChanged(nameof(HasReportBackups));
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Could not list report backups");
        }
    }

    /// <summary>Appelé par chaque ligne du tableau de backups via son own command.</summary>
    private async Task RevertReportToBackupAsync(BackupInfo backup)
    {
        var localDate = backup.TimestampUtc.ToLocalTime();
        var dateLabel = Strings.FormatLongDate(localDate);
        var confirm = ThemedMessageBox.Show(
            Strings.MsgRevertReportConfirm(dateLabel),
            Strings.MsgBoxRevertReport,
            MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
        if (confirm != MessageBoxResult.Yes) return;

        IsDeployingReport = true;
        ReportDeployStatus = $"Revert vers {dateLabel}…";
        try
        {
            var result = await _reportDeployer.RevertAsync(backup.FolderName, CancellationToken.None);
            ReportDeployStatus = result.Status == DeployStatus.Deployed
                ? Strings.MsgRevertReportSuccess(dateLabel)
                : $"✗ {result.ErrorMessage}";
            await LoadReportBackupsAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Revert report failed");
            ReportDeployStatus = $"✗ {ex.Message}";
        }
        finally
        {
            IsDeployingReport = false;
        }
    }

    // ===================== DOC TOOL BACKUPS (mirror of Report) =====================

    private async Task LoadDocBackupsAsync()
    {
        try
        {
            var list = await _docDeployer.ListBackupsAsync(CancellationToken.None);
            DocBackups.Clear();
            foreach (var b in list)
                DocBackups.Add(new DocBackupViewModel(b, RevertDocToBackupAsync));
            OnPropertyChanged(nameof(HasDocBackups));
        }
        catch (Exception ex)
        {
            _logger.LogDebug(ex, "Could not list doc backups");
        }
    }

    private async Task RevertDocToBackupAsync(BackupInfo backup)
    {
        var localDate = backup.TimestampUtc.ToLocalTime();
        var dateLabel = Strings.FormatLongDate(localDate);
        var confirm = ThemedMessageBox.Show(
            Strings.MsgRevertReportConfirm(dateLabel),
            Strings.MsgBoxRevertReport,
            MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No);
        if (confirm != MessageBoxResult.Yes) return;

        IsDeployingDoc = true;
        DocDeployStatus = $"Revert vers {dateLabel}…";
        try
        {
            var result = await _docDeployer.RevertAsync(backup.FolderName, CancellationToken.None);
            DocDeployStatus = result.Status == DeployStatus.Deployed
                ? Strings.MsgRevertReportSuccess(dateLabel)
                : $"✗ {result.ErrorMessage}";
            await LoadDocBackupsAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Revert doc failed");
            DocDeployStatus = $"✗ {ex.Message}";
        }
        finally
        {
            IsDeployingDoc = false;
        }
    }

    [RelayCommand]
    private async Task RedeployDocAsync()
    {
        var installed = _registry.Scan().OrderByDescending(v => v.Version).FirstOrDefault();
        if (installed is null)
        {
            DocDeployStatus = "Aucune version installée — installe d'abord une version qui contient _doc/.";
            return;
        }
        IsDeployingDoc = true;
        DocDeployStatus = $"Déploiement depuis v{installed.Version}…";
        try
        {
            Save();
            var progress = new Progress<DeployProgress>(dp =>
            {
                if (dp.FilesTotal > 0)
                    DocDeployStatus = $"{dp.FilesDone}/{dp.FilesTotal} : {dp.CurrentFilename}";
            });
            var result = await _docDeployer.DeployAsync(installed.FolderPath, progress, CancellationToken.None);
            DocDeployStatus = result.Status switch
            {
                DeployStatus.Deployed              => $"✓ {result.FilesDeployed} fichiers déployés vers {result.TargetPath}",
                DeployStatus.SkippedNoSourceFolder => $"⚠ La version v{installed.Version} ne contient pas de _doc/ — rien à déployer",
                DeployStatus.XamppNotFound         => $"✗ {result.ErrorMessage}",
                DeployStatus.Failed                => $"✗ {result.ErrorMessage}",
                _                                  => "?",
            };
            await LoadDocBackupsAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Redeploy doc failed");
            DocDeployStatus = $"✗ {ex.Message}";
        }
        finally
        {
            IsDeployingDoc = false;
        }
    }
}

/// <summary>
/// Ligne du tableau "Backups disponibles" dans Settings → Avancés → Outil Report.
/// Chaque instance porte sa propre commande Revert paramétrée pour ce backup.
/// </summary>
public sealed partial class ReportBackupViewModel : ObservableObject
{
    public BackupInfo Info { get; }
    public string DateDisplay => Strings.FormatLongDate(Info.TimestampUtc.ToLocalTime());
    public string SizeDisplay => Strings.SettingsReportBackupSize(Strings.FormatSize(Info.SizeBytes));
    public string FolderName => Info.FolderName;

    private readonly Func<BackupInfo, Task> _revertHandler;
    public ReportBackupViewModel(BackupInfo info, Func<BackupInfo, Task> revertHandler)
    {
        Info = info;
        _revertHandler = revertHandler;
    }

    [RelayCommand]
    private Task Revert() => _revertHandler(Info);
}

/// <summary>Variante doc — même structure que <see cref="ReportBackupViewModel"/>.</summary>
public sealed partial class DocBackupViewModel : ObservableObject
{
    public BackupInfo Info { get; }
    public string DateDisplay => Strings.FormatLongDate(Info.TimestampUtc.ToLocalTime());
    public string SizeDisplay => Strings.SettingsReportBackupSize(Strings.FormatSize(Info.SizeBytes));
    public string FolderName => Info.FolderName;

    private readonly Func<BackupInfo, Task> _revertHandler;
    public DocBackupViewModel(BackupInfo info, Func<BackupInfo, Task> revertHandler)
    {
        Info = info;
        _revertHandler = revertHandler;
    }

    [RelayCommand]
    private Task Revert() => _revertHandler(Info);
}
