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 ILogger _logger; private TenantProfile? _currentProfile; private bool _hasLocalSiteOverride; private IReadOnlyList _lastGroups = Array.Empty(); [ObservableProperty] private string _siteUrl = string.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, ILogger logger) : base(logger) { _duplicatesService = duplicatesService; _sessionManager = sessionManager; _htmlExportService = htmlExportService; _logger = logger; ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport); } protected override void OnGlobalSitesChanged(IReadOnlyList sites) { if (_hasLocalSiteOverride) return; SiteUrl = sites.Count > 0 ? sites[0].Url : string.Empty; } partial void OnSiteUrlChanged(string value) { if (string.IsNullOrWhiteSpace(value)) { _hasLocalSiteOverride = false; if (GlobalSites.Count > 0) SiteUrl = GlobalSites[0].Url; } else if (GlobalSites.Count == 0 || value != GlobalSites[0].Url) { _hasLocalSiteOverride = true; } } protected override async Task RunOperationAsync(CancellationToken ct, IProgress progress) { if (_currentProfile == null) { StatusMessage = "No tenant selected. Please connect to a tenant first."; return; } if (string.IsNullOrWhiteSpace(SiteUrl)) { StatusMessage = "Please enter a site URL."; return; } var siteProfile = new TenantProfile { TenantUrl = SiteUrl.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); _lastGroups = groups; // Flatten groups to display rows var rows = groups .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; _hasLocalSiteOverride = false; Results = new ObservableCollection(); _lastGroups = Array.Empty(); SiteUrl = string.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 { await _htmlExportService.WriteAsync(_lastGroups, dialog.FileName, CancellationToken.None); Process.Start(new ProcessStartInfo(dialog.FileName) { UseShellExecute = true }); } catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "HTML export failed."); } } }