using System.Collections.ObjectModel; using System.Diagnostics; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging; using Microsoft.Win32; using SharepointToolbox.Core.Models; using SharepointToolbox.Localization; using SharepointToolbox.Services; using SharepointToolbox.Services.Export; namespace SharepointToolbox.ViewModels.Tabs; public partial class VersionCleanupViewModel : FeatureViewModelBase { private readonly IVersionCleanupService _versionService; private readonly ISessionManager _sessionManager; private readonly VersionCleanupHtmlExportService _htmlExportService; private readonly IBrandingService _brandingService; private readonly ILogger _logger; private TenantProfile? _currentProfile; [ObservableProperty] private int _keepLast = 5; [ObservableProperty] private bool _keepFirstVersion; [ObservableProperty] private bool _confirmDelete = true; [ObservableProperty] private string _selectedLibrariesLabel = string.Empty; public ObservableCollection SelectedLibraries { get; } = new(); public ObservableCollection Results { get; } = new(); public long TotalBytesFreed => Results.Sum(r => r.BytesFreed); public int TotalVersionsDeleted => Results.Sum(r => r.VersionsDeleted); public int TotalFilesAffected => Results.Count(r => r.VersionsDeleted > 0); public bool HasResults => Results.Count > 0; public TenantProfile? CurrentProfile => _currentProfile; /// Set by the view to invoke against the current site. public Func, Task?>>? PickLibrariesAsync { get; set; } /// Set by the view to display a confirm dialog before destructive run. public Func? ConfirmAction { get; set; } public IAsyncRelayCommand SelectLibrariesCommand { get; } public IRelayCommand ClearLibrariesCommand { get; } public IAsyncRelayCommand ExportCsvCommand { get; } public IAsyncRelayCommand ExportHtmlCommand { get; } public VersionCleanupViewModel( IVersionCleanupService versionService, ISessionManager sessionManager, VersionCleanupHtmlExportService htmlExportService, IBrandingService brandingService, ILogger logger) : base(logger) { _versionService = versionService; _sessionManager = sessionManager; _htmlExportService = htmlExportService; _brandingService = brandingService; _logger = logger; SelectLibrariesCommand = new AsyncRelayCommand(SelectLibrariesAsync, CanPickLibraries); ClearLibrariesCommand = new RelayCommand(ClearLibraries); ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, () => Results.Count > 0); ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, () => Results.Count > 0); SelectedLibraries.CollectionChanged += (_, _) => UpdateSelectedLibrariesLabel(); Results.CollectionChanged += (_, _) => { OnPropertyChanged(nameof(HasResults)); OnPropertyChanged(nameof(TotalBytesFreed)); OnPropertyChanged(nameof(TotalVersionsDeleted)); OnPropertyChanged(nameof(TotalFilesAffected)); ExportCsvCommand.NotifyCanExecuteChanged(); ExportHtmlCommand.NotifyCanExecuteChanged(); }; UpdateSelectedLibrariesLabel(); } protected override void OnTenantSwitched(TenantProfile profile) { _currentProfile = profile; SelectedLibraries.Clear(); Results.Clear(); OnPropertyChanged(nameof(CurrentProfile)); SelectLibrariesCommand.NotifyCanExecuteChanged(); } protected override void OnGlobalSitesChanged(IReadOnlyList sites) { // Site changes invalidate library list — clear so user re-picks. SelectedLibraries.Clear(); SelectLibrariesCommand.NotifyCanExecuteChanged(); } private bool CanPickLibraries() => _currentProfile != null && GlobalSites.Count > 0; private async Task SelectLibrariesAsync() { if (PickLibrariesAsync == null || _currentProfile == null) return; var first = GlobalSites.FirstOrDefault(); if (first == null) { StatusMessage = TranslationSource.Instance["err.no_sites_selected"]; return; } var picked = await PickLibrariesAsync(first.Url, SelectedLibraries.ToArray()); if (picked == null) return; SelectedLibraries.Clear(); foreach (var t in picked) SelectedLibraries.Add(t); } private void ClearLibraries() => SelectedLibraries.Clear(); private void UpdateSelectedLibrariesLabel() { SelectedLibrariesLabel = SelectedLibraries.Count == 0 ? TranslationSource.Instance["versions.libs.all"] : string.Format(TranslationSource.Instance["versions.libs.count"], SelectedLibraries.Count); } protected override async Task RunOperationAsync(CancellationToken ct, IProgress progress) { if (_currentProfile == null) { StatusMessage = TranslationSource.Instance["err.no_tenant_connected"]; return; } var urls = GlobalSites.Select(s => s.Url).Where(u => !string.IsNullOrWhiteSpace(u)).ToList(); if (urls.Count == 0) { StatusMessage = TranslationSource.Instance["err.no_sites_selected"]; return; } if (KeepLast < 0) { StatusMessage = TranslationSource.Instance["versions.err.keepLast"]; return; } if (ConfirmDelete && ConfirmAction != null) { var msg = string.Format( TranslationSource.Instance["versions.confirm"], KeepLast, KeepFirstVersion ? TranslationSource.Instance["versions.confirm.keepFirst"] : string.Empty); if (!ConfirmAction(msg)) return; } var options = new VersionCleanupOptions( SelectedLibraries.ToList(), KeepLast, KeepFirstVersion); Results.Clear(); int siteIdx = 0; foreach (var url in urls) { ct.ThrowIfCancellationRequested(); siteIdx++; progress.Report(new OperationProgress(siteIdx, urls.Count, $"Cleaning {url}...")); var siteProfile = new TenantProfile { TenantUrl = url.TrimEnd('/'), ClientId = _currentProfile.ClientId, Name = _currentProfile.Name, }; var ctx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct); var siteResults = await _versionService.DeleteOldVersionsAsync(ctx, options, progress, ct); if (Application.Current?.Dispatcher is { } dispatcher) { await dispatcher.InvokeAsync(() => { foreach (var r in siteResults) Results.Add(r); }); } else { foreach (var r in siteResults) Results.Add(r); } } } private async Task ExportCsvAsync() { if (Results.Count == 0) return; var dialog = new SaveFileDialog { Title = "Export version cleanup results", Filter = "CSV files (*.csv)|*.csv", DefaultExt = "csv", FileName = "version_cleanup", }; if (dialog.ShowDialog() != true) return; try { using var w = new System.IO.StreamWriter(dialog.FileName); await w.WriteLineAsync("Site,Library,File,Versions Before,Versions Deleted,Versions Remaining,Bytes Freed,Error"); foreach (var r in Results) { await w.WriteLineAsync(string.Join(",", Csv(r.SiteUrl), Csv(r.Library), Csv(r.FileServerRelativeUrl), r.VersionsBefore, r.VersionsDeleted, r.VersionsRemaining, r.BytesFreed, Csv(r.Error ?? string.Empty))); } OpenFile(dialog.FileName); } catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "Version cleanup CSV export failed."); } } private async Task ExportHtmlAsync() { if (Results.Count == 0) return; var dialog = new SaveFileDialog { Title = "Export version cleanup results to HTML", Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*", DefaultExt = "html", FileName = "version_cleanup", }; if (dialog.ShowDialog() != true) return; try { var mspLogo = await _brandingService.GetMspLogoAsync(); var clientLogo = _currentProfile?.ClientLogo; var branding = new ReportBranding(mspLogo, clientLogo); await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding); OpenFile(dialog.FileName); } catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "Version cleanup HTML export failed."); } } private static void OpenFile(string filePath) { try { Process.Start(new ProcessStartInfo(filePath) { UseShellExecute = true }); } catch { } } private static string Csv(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) return "\"" + value.Replace("\"", "\"\"") + "\""; return value; } partial void OnKeepLastChanged(int value) { if (value < 0) KeepLast = 0; } }