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; public partial class SearchViewModel : FeatureViewModelBase { private readonly ISearchService _searchService; private readonly ISessionManager _sessionManager; private readonly SearchCsvExportService _csvExportService; private readonly SearchHtmlExportService _htmlExportService; private readonly IBrandingService _brandingService; private readonly ILogger _logger; private TenantProfile? _currentProfile; // ── Filter observable properties ───────────────────────────────────────── [ObservableProperty] private string _extensions = string.Empty; [ObservableProperty] private string _regex = string.Empty; [ObservableProperty] private bool _useCreatedAfter; [ObservableProperty] private DateTime _createdAfter = DateTime.Today.AddMonths(-1); [ObservableProperty] private bool _useCreatedBefore; [ObservableProperty] private DateTime _createdBefore = DateTime.Today; [ObservableProperty] private bool _useModifiedAfter; [ObservableProperty] private DateTime _modifiedAfter = DateTime.Today.AddMonths(-1); [ObservableProperty] private bool _useModifiedBefore; [ObservableProperty] private DateTime _modifiedBefore = DateTime.Today; [ObservableProperty] private string _createdBy = string.Empty; [ObservableProperty] private string _modifiedBy = string.Empty; [ObservableProperty] private string _library = string.Empty; [ObservableProperty] private int _maxResults = 5000; private ObservableCollection _results = new(); public ObservableCollection Results { get => _results; private set { _results = value; OnPropertyChanged(); ExportCsvCommand.NotifyCanExecuteChanged(); ExportHtmlCommand.NotifyCanExecuteChanged(); } } public IAsyncRelayCommand ExportCsvCommand { get; } public IAsyncRelayCommand ExportHtmlCommand { get; } public TenantProfile? CurrentProfile => _currentProfile; public SearchViewModel( ISearchService searchService, ISessionManager sessionManager, SearchCsvExportService csvExportService, SearchHtmlExportService htmlExportService, IBrandingService brandingService, ILogger logger) : base(logger) { _searchService = searchService; _sessionManager = sessionManager; _csvExportService = csvExportService; _htmlExportService = htmlExportService; _brandingService = brandingService; _logger = logger; ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport); 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 allItems = 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 SearchOptions( Extensions: ParseExtensions(Extensions), Regex: string.IsNullOrWhiteSpace(Regex) ? null : Regex, CreatedAfter: UseCreatedAfter ? CreatedAfter : null, CreatedBefore: UseCreatedBefore ? CreatedBefore : null, ModifiedAfter: UseModifiedAfter ? ModifiedAfter : null, ModifiedBefore: UseModifiedBefore ? ModifiedBefore : null, CreatedBy: string.IsNullOrWhiteSpace(CreatedBy) ? null : CreatedBy, ModifiedBy: string.IsNullOrWhiteSpace(ModifiedBy) ? null : ModifiedBy, Library: string.IsNullOrWhiteSpace(Library) ? null : Library, MaxResults: Math.Clamp(MaxResults, 1, 50_000), SiteUrl: url.TrimEnd('/') ); var items = await _searchService.SearchFilesAsync(ctx, opts, progress, ct); allItems.AddRange(items); } if (Application.Current?.Dispatcher is { } dispatcher) await dispatcher.InvokeAsync(() => Results = new ObservableCollection(allItems)); else Results = new ObservableCollection(allItems); } protected override void OnTenantSwitched(Core.Models.TenantProfile profile) { _currentProfile = profile; Results = new ObservableCollection(); OnPropertyChanged(nameof(CurrentProfile)); ExportCsvCommand.NotifyCanExecuteChanged(); ExportHtmlCommand.NotifyCanExecuteChanged(); } internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile; private bool CanExport() => Results.Count > 0; private async Task ExportCsvAsync() { if (Results.Count == 0) return; var dialog = new SaveFileDialog { Title = "Export search results to CSV", Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*", DefaultExt = "csv", FileName = "search_results" }; if (dialog.ShowDialog() != true) return; try { await _csvExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None); OpenFile(dialog.FileName); } catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "CSV export failed."); } } private async Task ExportHtmlAsync() { if (Results.Count == 0) return; var dialog = new SaveFileDialog { Title = "Export search results to HTML", Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*", DefaultExt = "html", FileName = "search_results" }; 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(Results, dialog.FileName, CancellationToken.None, branding); OpenFile(dialog.FileName); } catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "HTML export failed."); } } private static string[] ParseExtensions(string input) { if (string.IsNullOrWhiteSpace(input)) return Array.Empty(); return input.Split(new[] { ' ', ',', ';' }, StringSplitOptions.RemoveEmptyEntries) .Select(e => e.TrimStart('.').ToLowerInvariant()) .Where(e => e.Length > 0) .Distinct() .ToArray(); } private static void OpenFile(string filePath) { try { Process.Start(new ProcessStartInfo(filePath) { UseShellExecute = true }); } catch { } } }