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.Services; using SharepointToolbox.Services.Export; namespace SharepointToolbox.ViewModels.Tabs; /// Flat display row wrapping a DuplicateItem with its group name. public class DuplicateRow { public string GroupName { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string Path { get; set; } = string.Empty; public string Library { get; set; } = string.Empty; public long? SizeBytes { get; set; } public DateTime? Created { get; set; } public DateTime? Modified { get; set; } public int? FolderCount { get; set; } public int? FileCount { get; set; } public int GroupSize { get; set; } } public partial class DuplicatesViewModel : FeatureViewModelBase { private readonly IDuplicatesService _duplicatesService; private readonly ISessionManager _sessionManager; private readonly DuplicatesHtmlExportService _htmlExportService; private readonly IBrandingService _brandingService; private readonly ILogger _logger; private TenantProfile? _currentProfile; private IReadOnlyList _lastGroups = Array.Empty(); [ObservableProperty] private bool _modeFiles = true; [ObservableProperty] private bool _modeFolders; [ObservableProperty] private bool _matchSize = true; [ObservableProperty] private bool _matchCreated; [ObservableProperty] private bool _matchModified; [ObservableProperty] private bool _matchSubfolders; [ObservableProperty] private bool _matchFileCount; [ObservableProperty] private bool _includeSubsites; [ObservableProperty] private string _library = string.Empty; private ObservableCollection _results = new(); public ObservableCollection Results { get => _results; private set { _results = value; OnPropertyChanged(); ExportHtmlCommand.NotifyCanExecuteChanged(); } } public IAsyncRelayCommand ExportHtmlCommand { get; } public TenantProfile? CurrentProfile => _currentProfile; public DuplicatesViewModel( IDuplicatesService duplicatesService, ISessionManager sessionManager, DuplicatesHtmlExportService htmlExportService, IBrandingService brandingService, ILogger logger) : base(logger) { _duplicatesService = duplicatesService; _sessionManager = sessionManager; _htmlExportService = htmlExportService; _brandingService = brandingService; _logger = logger; ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport); } protected override async Task RunOperationAsync(CancellationToken ct, IProgress progress) { if (_currentProfile == null) { StatusMessage = "No tenant selected. Please connect to a tenant first."; return; } var urls = GlobalSites.Select(s => s.Url).Where(u => !string.IsNullOrWhiteSpace(u)).ToList(); if (urls.Count == 0) { StatusMessage = "Select at least one site from the toolbar."; return; } var allGroups = new List(); foreach (var url in urls) { var siteProfile = new TenantProfile { TenantUrl = url.TrimEnd('/'), ClientId = _currentProfile.ClientId, Name = _currentProfile.Name }; var ctx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct); var opts = new DuplicateScanOptions( Mode: ModeFiles ? "Files" : "Folders", MatchSize: MatchSize, MatchCreated: MatchCreated, MatchModified: MatchModified, MatchSubfolderCount: MatchSubfolders, MatchFileCount: MatchFileCount, IncludeSubsites: IncludeSubsites, Library: string.IsNullOrWhiteSpace(Library) ? null : Library ); var groups = await _duplicatesService.ScanDuplicatesAsync(ctx, opts, progress, ct); allGroups.AddRange(groups); } _lastGroups = allGroups; // Flatten groups to display rows var rows = allGroups .SelectMany(g => g.Items.Select(item => new DuplicateRow { GroupName = g.Name, Name = item.Name, Path = item.Path, Library = item.Library, SizeBytes = item.SizeBytes, Created = item.Created, Modified = item.Modified, FolderCount = item.FolderCount, FileCount = item.FileCount, GroupSize = g.Items.Count })) .ToList(); if (Application.Current?.Dispatcher is { } dispatcher) await dispatcher.InvokeAsync(() => Results = new ObservableCollection(rows)); else Results = new ObservableCollection(rows); } protected override void OnTenantSwitched(Core.Models.TenantProfile profile) { _currentProfile = profile; Results = new ObservableCollection(); _lastGroups = Array.Empty(); OnPropertyChanged(nameof(CurrentProfile)); ExportHtmlCommand.NotifyCanExecuteChanged(); } internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile; private bool CanExport() => _lastGroups.Count > 0; private async Task ExportHtmlAsync() { if (_lastGroups.Count == 0) return; var dialog = new SaveFileDialog { Title = "Export duplicates report to HTML", Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*", DefaultExt = "html", FileName = "duplicates_report" }; if (dialog.ShowDialog() != true) return; try { ReportBranding? branding = null; if (_brandingService is not null) { var mspLogo = await _brandingService.GetMspLogoAsync(); var clientLogo = _currentProfile?.ClientLogo; branding = new ReportBranding(mspLogo, clientLogo); } await _htmlExportService.WriteAsync(_lastGroups, dialog.FileName, CancellationToken.None, branding); Process.Start(new ProcessStartInfo(dialog.FileName) { UseShellExecute = true }); } catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "HTML export failed."); } } }