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 StorageViewModel : FeatureViewModelBase { private readonly IStorageService _storageService; private readonly ISessionManager _sessionManager; private readonly StorageCsvExportService _csvExportService; private readonly StorageHtmlExportService _htmlExportService; private readonly ILogger _logger; private TenantProfile? _currentProfile; [ObservableProperty] private string _siteUrl = string.Empty; [ObservableProperty] private bool _perLibrary = true; [ObservableProperty] private bool _includeSubsites; [ObservableProperty] private int _folderDepth; public bool IsMaxDepth { get => FolderDepth >= 999; set { if (value) FolderDepth = 999; else if (FolderDepth >= 999) FolderDepth = 0; OnPropertyChanged(); } } 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 StorageViewModel( IStorageService storageService, ISessionManager sessionManager, StorageCsvExportService csvExportService, StorageHtmlExportService htmlExportService, ILogger logger) : base(logger) { _storageService = storageService; _sessionManager = sessionManager; _csvExportService = csvExportService; _htmlExportService = htmlExportService; _logger = logger; ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport); ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport); } /// Test constructor — omits export services. internal StorageViewModel( IStorageService storageService, ISessionManager sessionManager, ILogger logger) : base(logger) { _storageService = storageService; _sessionManager = sessionManager; _csvExportService = null!; _htmlExportService = null!; _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; } if (string.IsNullOrWhiteSpace(SiteUrl)) { StatusMessage = "Please enter a site URL."; return; } // Build a site-specific profile: same ClientId and Name, but TenantUrl points to the // site URL the user entered (may differ from the tenant root). var siteProfile = new TenantProfile { TenantUrl = SiteUrl.TrimEnd('/'), ClientId = _currentProfile.ClientId, Name = _currentProfile.Name }; var ctx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct); var options = new StorageScanOptions( PerLibrary: PerLibrary, IncludeSubsites: IncludeSubsites, FolderDepth: FolderDepth); var nodes = await _storageService.CollectStorageAsync(ctx, options, progress, ct); // Flatten tree to one level for DataGrid display (assign IndentLevel during flatten) var flat = new List(); foreach (var node in nodes) FlattenNode(node, 0, flat); if (Application.Current?.Dispatcher is { } dispatcher) { await dispatcher.InvokeAsync(() => { Results = new ObservableCollection(flat); }); } else { Results = new ObservableCollection(flat); } } protected override void OnTenantSwitched(TenantProfile profile) { _currentProfile = profile; Results = new ObservableCollection(); SiteUrl = string.Empty; OnPropertyChanged(nameof(CurrentProfile)); ExportCsvCommand.NotifyCanExecuteChanged(); ExportHtmlCommand.NotifyCanExecuteChanged(); } internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile; internal Task TestRunOperationAsync(CancellationToken ct, IProgress progress) => RunOperationAsync(ct, progress); private bool CanExport() => Results.Count > 0; private async Task ExportCsvAsync() { if (Results.Count == 0) return; var dialog = new SaveFileDialog { Title = "Export storage metrics to CSV", Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*", DefaultExt = "csv", FileName = "storage_metrics" }; 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 storage metrics to HTML", Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*", DefaultExt = "html", FileName = "storage_metrics" }; if (dialog.ShowDialog() != true) return; try { await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None); OpenFile(dialog.FileName); } catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "HTML export failed."); } } private static void FlattenNode(StorageNode node, int level, List result) { node.IndentLevel = level; result.Add(node); foreach (var child in node.Children) FlattenNode(child, level + 1, result); } private static void OpenFile(string filePath) { try { Process.Start(new ProcessStartInfo(filePath) { UseShellExecute = true }); } catch { /* ignore — file may open but this is best-effort */ } } }