using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Data;
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;

/// <summary>
/// ViewModel for the User Access Audit tab.
/// Orchestrates people-picker search, site selection, audit execution,
/// result grouping/filtering, summary banner, and export commands.
/// </summary>
public partial class UserAccessAuditViewModel : FeatureViewModelBase
{
    private readonly IUserAccessAuditService _auditService;
    private readonly IGraphUserSearchService _graphUserSearchService;
    private readonly ISessionManager _sessionManager;
    private readonly UserAccessCsvExportService? _csvExportService;
    private readonly UserAccessHtmlExportService? _htmlExportService;
    private readonly IBrandingService? _brandingService;
    private readonly IGraphUserDirectoryService? _graphUserDirectoryService;
    private readonly IOwnershipElevationService? _ownershipService;
    private readonly SettingsService? _settingsService;
    private readonly ILogger<FeatureViewModelBase> _logger;

    // ── People picker debounce ──────────────────────────────────────────────

    private CancellationTokenSource? _searchCts;

    // ── Observable properties ───────────────────────────────────────────────

    /// <summary>Text typed in the people-picker search box. Changes trigger debounced Graph search.</summary>
    [ObservableProperty]
    private string _searchQuery = string.Empty;

    /// <summary>Autocomplete dropdown items from Graph search results.</summary>
    [ObservableProperty]
    private ObservableCollection<GraphUserResult> _searchResults = new();

    /// <summary>Users added for audit.</summary>
    [ObservableProperty]
    private ObservableCollection<GraphUserResult> _selectedUsers = new();

    /// <summary>Raw audit output entries.</summary>
    [ObservableProperty]
    private ObservableCollection<UserAccessEntry> _results = new();

    /// <summary>Real-time text filter applied to ResultsView.</summary>
    [ObservableProperty]
    private string _filterText = string.Empty;

    /// <summary>When true, results are grouped by user; when false, grouped by site.</summary>
    [ObservableProperty]
    private bool _isGroupByUser = true;

    /// <summary>Include inherited permissions in scan.</summary>
    [ObservableProperty]
    private bool _includeInherited;

    /// <summary>Include folders in scan.</summary>
    [ObservableProperty]
    private bool _scanFolders = true;

    /// <summary>Include subsites in scan.</summary>
    [ObservableProperty]
    private bool _includeSubsites;

    /// <summary>True while a Graph user search is in progress.</summary>
    [ObservableProperty]
    private bool _isSearching;

    // ── Directory browse mode properties ───────────────────────────────────

    /// <summary>When true, the UI shows the directory browse panel instead of the people-picker search.</summary>
    [ObservableProperty]
    private bool _isBrowseMode;

    /// <summary>All directory users loaded from Graph.</summary>
    [ObservableProperty]
    private ObservableCollection<GraphDirectoryUser> _directoryUsers = new();

    /// <summary>True while a directory load is in progress.</summary>
    [ObservableProperty]
    private bool _isLoadingDirectory;

    /// <summary>Status text for directory load progress, e.g. "Loading... 500 users".</summary>
    [ObservableProperty]
    private string _directoryLoadStatus = string.Empty;

    /// <summary>When true, guest users are shown in the directory view; when false, only members.</summary>
    [ObservableProperty]
    private bool _includeGuests;

    /// <summary>Text filter applied to DirectoryUsersView (DisplayName, UPN, Department, JobTitle).</summary>
    [ObservableProperty]
    private string _directoryFilterText = string.Empty;

    /// <summary>When true, the CSV export merges duplicate permission rows into consolidated entries.</summary>
    [ObservableProperty]
    private bool _mergePermissions;

    /// <summary>0 = Single file, 1 = Split by site, 2 = Split by user.</summary>
    [ObservableProperty] private int _splitModeIndex;
    /// <summary>0 = Separate HTML files, 1 = Single tabbed HTML.</summary>
    [ObservableProperty] private int _htmlLayoutIndex;

    private ReportSplitMode CurrentSplit => SplitModeIndex switch
    {
        1 => ReportSplitMode.BySite,
        2 => ReportSplitMode.ByUser,
        _ => ReportSplitMode.Single
    };
    private HtmlSplitLayout CurrentLayout => HtmlLayoutIndex == 1 ? HtmlSplitLayout.SingleTabbed : HtmlSplitLayout.SeparateFiles;

    private CancellationTokenSource? _directoryCts = null;

    // ── Computed summary properties ─────────────────────────────────────────

    /// <summary>Total number of access entries in current results.</summary>
    public int TotalAccessCount => Results.Count;

    /// <summary>Number of distinct sites referenced in current results.</summary>
    public int SitesCount => Results.Select(r => r.SiteUrl).Distinct().Count();

    /// <summary>Number of high-privilege access entries in current results.</summary>
    public int HighPrivilegeCount => Results.Count(r => r.IsHighPrivilege);

    /// <summary>Human-readable label for the people-picker selection, e.g. "2 user(s) selected".</summary>
    public string SelectedUsersLabel =>
        SelectedUsers.Count > 0
            ? $"{SelectedUsers.Count} user(s) selected"
            : string.Empty;

    /// <summary>Number of users currently visible in the filtered directory view.</summary>
    public int DirectoryUserCount => DirectoryUsersView?.Cast<object>().Count() ?? 0;

    // ── CollectionViewSource (grouping + filtering) ─────────────────────────

    /// <summary>ICollectionView over Results supporting grouping and text filtering.</summary>
    public ICollectionView ResultsView { get; }

    /// <summary>ICollectionView over DirectoryUsers with member/guest and text filtering, sorted by DisplayName.</summary>
    public ICollectionView DirectoryUsersView { get; }

    // ── Commands ────────────────────────────────────────────────────────────

    public IAsyncRelayCommand ExportCsvCommand { get; }
    public IAsyncRelayCommand ExportHtmlCommand { get; }
    public RelayCommand<GraphUserResult> AddUserCommand { get; }
    public RelayCommand<GraphUserResult> RemoveUserCommand { get; }
    public RelayCommand<GraphDirectoryUser> SelectDirectoryUserCommand { get; }
    public IAsyncRelayCommand LoadDirectoryCommand { get; }
    public RelayCommand CancelDirectoryLoadCommand { get; }

    // ── Current tenant profile ──────────────────────────────────────────────

    internal TenantProfile? _currentProfile;

    /// <summary>Public accessor for the current tenant profile — used by View layer dialog factory.</summary>
    public TenantProfile? CurrentProfile => _currentProfile;

    // ── Constructors ────────────────────────────────────────────────────────

    /// <summary>Full constructor — used by DI and production code.</summary>
    public UserAccessAuditViewModel(
        IUserAccessAuditService auditService,
        IGraphUserSearchService graphUserSearchService,
        ISessionManager sessionManager,
        UserAccessCsvExportService csvExportService,
        UserAccessHtmlExportService htmlExportService,
        IBrandingService brandingService,
        IGraphUserDirectoryService graphUserDirectoryService,
        ILogger<FeatureViewModelBase> logger,
        IOwnershipElevationService? ownershipService = null,
        SettingsService? settingsService = null)
        : base(logger)
    {
        _auditService = auditService;
        _graphUserSearchService = graphUserSearchService;
        _sessionManager = sessionManager;
        _csvExportService = csvExportService;
        _htmlExportService = htmlExportService;
        _brandingService = brandingService;
        _graphUserDirectoryService = graphUserDirectoryService;
        _ownershipService = ownershipService;
        _settingsService = settingsService;
        _logger = logger;

        ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
        ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
        AddUserCommand = new RelayCommand<GraphUserResult>(ExecuteAddUser);
        RemoveUserCommand = new RelayCommand<GraphUserResult>(ExecuteRemoveUser);
        SelectDirectoryUserCommand = new RelayCommand<GraphDirectoryUser>(ExecuteSelectDirectoryUser);

        SelectedUsers.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SelectedUsersLabel));

        var cvs = new CollectionViewSource { Source = Results };
        ResultsView = cvs.View;
        ApplyGrouping();

        var dirCvs = new CollectionViewSource { Source = DirectoryUsers };
        DirectoryUsersView = dirCvs.View;
        DirectoryUsersView.SortDescriptions.Add(
            new SortDescription(nameof(GraphDirectoryUser.DisplayName), ListSortDirection.Ascending));
        DirectoryUsersView.Filter = DirectoryFilterPredicate;

        LoadDirectoryCommand = new AsyncRelayCommand(LoadDirectoryAsync, () => !IsLoadingDirectory);
        CancelDirectoryLoadCommand = new RelayCommand(
            () => _directoryCts?.Cancel(),
            () => IsLoadingDirectory);
    }

    /// <summary>Test constructor — omits export services (not needed for unit tests).</summary>
    internal UserAccessAuditViewModel(
        IUserAccessAuditService auditService,
        IGraphUserSearchService graphUserSearchService,
        ISessionManager sessionManager,
        ILogger<FeatureViewModelBase> logger,
        IBrandingService? brandingService = null,
        IGraphUserDirectoryService? graphUserDirectoryService = null)
        : base(logger)
    {
        _auditService = auditService;
        _graphUserSearchService = graphUserSearchService;
        _sessionManager = sessionManager;
        _csvExportService = null;
        _htmlExportService = null;
        _brandingService = brandingService;
        _graphUserDirectoryService = graphUserDirectoryService;
        _logger = logger;

        ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport);
        ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
        AddUserCommand = new RelayCommand<GraphUserResult>(ExecuteAddUser);
        RemoveUserCommand = new RelayCommand<GraphUserResult>(ExecuteRemoveUser);
        SelectDirectoryUserCommand = new RelayCommand<GraphDirectoryUser>(ExecuteSelectDirectoryUser);

        SelectedUsers.CollectionChanged += (_, _) => OnPropertyChanged(nameof(SelectedUsersLabel));

        var cvs = new CollectionViewSource { Source = Results };
        ResultsView = cvs.View;
        ApplyGrouping();

        var dirCvs = new CollectionViewSource { Source = DirectoryUsers };
        DirectoryUsersView = dirCvs.View;
        DirectoryUsersView.SortDescriptions.Add(
            new SortDescription(nameof(GraphDirectoryUser.DisplayName), ListSortDirection.Ascending));
        DirectoryUsersView.Filter = DirectoryFilterPredicate;

        LoadDirectoryCommand = new AsyncRelayCommand(LoadDirectoryAsync, () => !IsLoadingDirectory);
        CancelDirectoryLoadCommand = new RelayCommand(
            () => _directoryCts?.Cancel(),
            () => IsLoadingDirectory);
    }

    // ── FeatureViewModelBase implementation ─────────────────────────────────

    protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
    {
        if (SelectedUsers.Count == 0)
        {
            StatusMessage = TranslationSource.Instance["err.no_users_selected"];
            return;
        }

        if (GlobalSites.Count == 0)
        {
            StatusMessage = TranslationSource.Instance["err.no_sites_selected"];
            return;
        }

        var effectiveSites = GlobalSites.ToList();

        var userLogins = SelectedUsers.Select(u => u.UserPrincipalName).ToList();
        var scanOptions = new ScanOptions(
            IncludeInherited: IncludeInherited,
            ScanFolders: ScanFolders,
            FolderDepth: 1,
            IncludeSubsites: IncludeSubsites);

        if (_currentProfile == null)
        {
            StatusMessage = TranslationSource.Instance["err.no_profile_selected"];
            return;
        }

        var autoOwnership = await IsAutoTakeOwnershipEnabled();

        Func<string, CancellationToken, Task<bool>>? onAccessDenied = null;
        if (_ownershipService != null && autoOwnership)
        {
            onAccessDenied = async (siteUrl, token) =>
            {
                try
                {
                    _logger.LogWarning("Access denied on {Url}, auto-elevating ownership...", siteUrl);
                    var adminUrl = DeriveAdminUrl(_currentProfile?.TenantUrl ?? siteUrl);
                    var adminProfile = new TenantProfile
                    {
                        TenantUrl = adminUrl,
                        ClientId = _currentProfile?.ClientId ?? string.Empty,
                        Name = _currentProfile?.Name ?? string.Empty
                    };
                    var adminCtx = await _sessionManager.GetOrCreateContextAsync(adminProfile, token);
                    await _ownershipService.ElevateAsync(adminCtx, siteUrl, string.Empty, token);
                    return true;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Auto-elevation failed for {Url}", siteUrl);
                    return false;
                }
            };
        }

        var entries = await _auditService.AuditUsersAsync(
            _sessionManager,
            _currentProfile,
            userLogins,
            effectiveSites,
            scanOptions,
            progress,
            ct,
            onAccessDenied);

        // Update Results on the UI thread — clear + repopulate (not replace)
        // so the CollectionViewSource bound to ResultsView stays connected.
        var dispatcher = Application.Current?.Dispatcher;
        if (dispatcher != null)
        {
            await dispatcher.InvokeAsync(() =>
            {
                Results.Clear();
                foreach (var entry in entries)
                    Results.Add(entry);
                NotifySummaryProperties();
            });
        }
        else
        {
            Results.Clear();
            foreach (var entry in entries)
                Results.Add(entry);
            NotifySummaryProperties();
        }

        ExportCsvCommand.NotifyCanExecuteChanged();
        ExportHtmlCommand.NotifyCanExecuteChanged();
    }

    // ── Auto-ownership helpers ───────────────────────────────────────────────

    private async Task<bool> IsAutoTakeOwnershipEnabled()
    {
        if (_settingsService == null) return false;
        var settings = await _settingsService.GetSettingsAsync();
        return settings.AutoTakeOwnership;
    }

    internal static string DeriveAdminUrl(string tenantUrl)
    {
        var uri = new Uri(tenantUrl.TrimEnd('/'));
        var host = uri.Host;
        if (host.Contains("-admin.sharepoint.com", StringComparison.OrdinalIgnoreCase))
            return tenantUrl;
        var adminHost = host.Replace(".sharepoint.com", "-admin.sharepoint.com",
            StringComparison.OrdinalIgnoreCase);
        return $"{uri.Scheme}://{adminHost}";
    }

    // ── Tenant switching ─────────────────────────────────────────────────────

    protected override void OnTenantSwitched(TenantProfile profile)
    {
        _currentProfile = profile;
        Results.Clear();
        SelectedUsers.Clear();
        SearchQuery = string.Empty;
        SearchResults.Clear();
        FilterText = string.Empty;
        OnPropertyChanged(nameof(CurrentProfile));
        NotifySummaryProperties();
        ExportCsvCommand.NotifyCanExecuteChanged();
        ExportHtmlCommand.NotifyCanExecuteChanged();

        // Directory browse mode reset
        _directoryCts?.Cancel();
        _directoryCts?.Dispose();
        _directoryCts = null;
        DirectoryUsers.Clear();
        DirectoryFilterText = string.Empty;
        DirectoryLoadStatus = string.Empty;
        IsBrowseMode = false;
        IsLoadingDirectory = false;
        IncludeGuests = false;
        OnPropertyChanged(nameof(DirectoryUserCount));
    }

    // ── Observable property change handlers ─────────────────────────────────

    partial void OnSearchQueryChanged(string value)
    {
        // Cancel any in-flight search
        _searchCts?.Cancel();
        _searchCts?.Dispose();
        _searchCts = new CancellationTokenSource();

        var ct = _searchCts.Token;
        _ = DebounceSearchAsync(value, ct);
    }

    partial void OnFilterTextChanged(string value)
    {
        ResultsView.Refresh();
    }

    partial void OnIsGroupByUserChanged(bool value)
    {
        ApplyGrouping();
    }

    partial void OnResultsChanged(ObservableCollection<UserAccessEntry> value)
    {
        // Safety net: if the collection reference ever changes, rebind grouping/filter
        ApplyGrouping();
        ResultsView.Filter = FilterPredicate;
        ResultsView.Refresh();
        NotifySummaryProperties();
    }

    partial void OnIncludeGuestsChanged(bool value)
    {
        DirectoryUsersView.Refresh();
        OnPropertyChanged(nameof(DirectoryUserCount));
    }

    partial void OnDirectoryFilterTextChanged(string value)
    {
        DirectoryUsersView.Refresh();
        OnPropertyChanged(nameof(DirectoryUserCount));
    }

    partial void OnIsLoadingDirectoryChanged(bool value)
    {
        LoadDirectoryCommand.NotifyCanExecuteChanged();
        CancelDirectoryLoadCommand.NotifyCanExecuteChanged();
    }

    // ── Internal helpers ─────────────────────────────────────────────────────

    /// <summary>Sets the current tenant profile (for test injection).</summary>
    internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile;

    /// <summary>Exposes RunOperationAsync for unit tests (internal + InternalsVisibleTo).</summary>
    internal Task TestRunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
        => RunOperationAsync(ct, progress);

    // ── Directory browse mode ──────────────────────────────────────────────

    private async Task LoadDirectoryAsync()
    {
        if (_graphUserDirectoryService is null) return;
        var clientId = _currentProfile?.ClientId;
        if (string.IsNullOrEmpty(clientId))
        {
            StatusMessage = TranslationSource.Instance["err.no_profile_selected"];
            return;
        }

        _directoryCts?.Cancel();
        _directoryCts?.Dispose();
        _directoryCts = new CancellationTokenSource();
        var ct = _directoryCts.Token;

        IsLoadingDirectory = true;
        DirectoryLoadStatus = "Loading...";
        try
        {
            var progress = new Progress<int>(count =>
                DirectoryLoadStatus = $"Loading... {count} users");

            var users = await _graphUserDirectoryService.GetUsersAsync(
                clientId, includeGuests: true, progress, ct);

            ct.ThrowIfCancellationRequested();

            var dispatcher = Application.Current?.Dispatcher;
            if (dispatcher != null)
            {
                await dispatcher.InvokeAsync(() => PopulateDirectory(users));
            }
            else
            {
                PopulateDirectory(users);
            }

            DirectoryLoadStatus = $"{users.Count} users loaded";
        }
        catch (OperationCanceledException)
        {
            DirectoryLoadStatus = "Load cancelled.";
        }
        catch (Exception ex)
        {
            DirectoryLoadStatus = $"Failed: {ex.Message}";
            _logger.LogError(ex, "Directory load failed.");
        }
        finally
        {
            IsLoadingDirectory = false;
        }
    }

    private void PopulateDirectory(IReadOnlyList<GraphDirectoryUser> users)
    {
        DirectoryUsers.Clear();
        foreach (var u in users)
            DirectoryUsers.Add(u);
        DirectoryUsersView.Refresh();
        OnPropertyChanged(nameof(DirectoryUserCount));
    }

    private bool DirectoryFilterPredicate(object obj)
    {
        if (obj is not GraphDirectoryUser user) return false;

        // Member/guest filter
        if (!IncludeGuests && !string.Equals(user.UserType, "Member", StringComparison.OrdinalIgnoreCase))
            return false;

        // Text filter
        if (string.IsNullOrWhiteSpace(DirectoryFilterText)) return true;
        var filter = DirectoryFilterText.Trim();
        return user.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)
            || user.UserPrincipalName.Contains(filter, StringComparison.OrdinalIgnoreCase)
            || (user.Department?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false)
            || (user.JobTitle?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);
    }

    /// <summary>Exposes LoadDirectoryAsync for unit tests (internal + InternalsVisibleTo).</summary>
    internal Task TestLoadDirectoryAsync() => LoadDirectoryAsync();

    // ── Command implementations ───────────────────────────────────────────────

    private bool CanExport() => Results.Count > 0;

    private async Task ExportCsvAsync()
    {
        if (_csvExportService == null || Results.Count == 0) return;
        var dialog = new SaveFileDialog
        {
            Title = "Export user access audit to CSV",
            Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*",
            DefaultExt = "csv",
            FileName = "user-access-audit"
        };
        if (dialog.ShowDialog() != true) return;
        try
        {
            await _csvExportService.WriteAsync((IReadOnlyList<UserAccessEntry>)Results, dialog.FileName, CurrentSplit, CancellationToken.None, MergePermissions);
            OpenFile(dialog.FileName);
        }
        catch (Exception ex)
        {
            StatusMessage = $"Export failed: {ex.Message}";
            _logger.LogError(ex, "CSV export failed.");
        }
    }

    private async Task ExportHtmlAsync()
    {
        if (_htmlExportService == null || Results.Count == 0) return;
        var dialog = new SaveFileDialog
        {
            Title = "Export user access audit to HTML",
            Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*",
            DefaultExt = "html",
            FileName = "user-access-audit"
        };
        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((IReadOnlyList<UserAccessEntry>)Results, dialog.FileName, CurrentSplit, CurrentLayout, CancellationToken.None, MergePermissions, branding);
            OpenFile(dialog.FileName);
        }
        catch (Exception ex)
        {
            StatusMessage = $"Export failed: {ex.Message}";
            _logger.LogError(ex, "HTML export failed.");
        }
    }

    private void ExecuteAddUser(GraphUserResult? user)
    {
        if (user == null) return;
        if (!SelectedUsers.Any(u => u.UserPrincipalName == user.UserPrincipalName))
        {
            SelectedUsers.Add(user);
        }
        // Clear search after adding
        SearchQuery = string.Empty;
        SearchResults.Clear();
    }

    private void ExecuteRemoveUser(GraphUserResult? user)
    {
        if (user == null) return;
        SelectedUsers.Remove(user);
    }

    private void ExecuteSelectDirectoryUser(GraphDirectoryUser? dirUser)
    {
        if (dirUser == null) return;
        var userResult = new GraphUserResult(dirUser.DisplayName, dirUser.UserPrincipalName, dirUser.Mail);
        if (!SelectedUsers.Any(u => u.UserPrincipalName == userResult.UserPrincipalName))
        {
            SelectedUsers.Add(userResult);
        }
    }

    private async Task DebounceSearchAsync(string query, CancellationToken ct)
    {
        try
        {
            await Task.Delay(300, ct);
            ct.ThrowIfCancellationRequested();

            if (string.IsNullOrWhiteSpace(query) || query.Length < 2)
            {
                SearchResults.Clear();
                return;
            }

            IsSearching = true;
            try
            {
                var clientId = _currentProfile?.ClientId ?? string.Empty;
                var results = await _graphUserSearchService.SearchUsersAsync(clientId, query, 10, ct);

                ct.ThrowIfCancellationRequested();

                var dispatcher = Application.Current?.Dispatcher;
                if (dispatcher != null)
                {
                    await dispatcher.InvokeAsync(() =>
                    {
                        SearchResults.Clear();
                        foreach (var r in results)
                            SearchResults.Add(r);
                    });
                }
                else
                {
                    SearchResults.Clear();
                    foreach (var r in results)
                        SearchResults.Add(r);
                }
            }
            finally
            {
                IsSearching = false;
            }
        }
        catch (OperationCanceledException)
        {
            // Keystroke cancelled this search — expected, ignore
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "People picker search failed for query '{Query}'.", query);
            IsSearching = false;
        }
    }

    private void ApplyGrouping()
    {
        ResultsView.GroupDescriptions.Clear();
        if (IsGroupByUser)
            ResultsView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(UserAccessEntry.UserLogin)));
        else
            ResultsView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(UserAccessEntry.SiteUrl)));

        ResultsView.Filter = FilterPredicate;
    }

    private bool FilterPredicate(object obj)
    {
        if (string.IsNullOrWhiteSpace(FilterText)) return true;
        if (obj is not UserAccessEntry entry) return false;

        var filter = FilterText.Trim();
        return entry.UserDisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)
            || entry.UserLogin.Contains(filter, StringComparison.OrdinalIgnoreCase)
            || entry.SiteTitle.Contains(filter, StringComparison.OrdinalIgnoreCase)
            || entry.SiteUrl.Contains(filter, StringComparison.OrdinalIgnoreCase)
            || entry.ObjectTitle.Contains(filter, StringComparison.OrdinalIgnoreCase)
            || entry.PermissionLevel.Contains(filter, StringComparison.OrdinalIgnoreCase);
    }

    private void NotifySummaryProperties()
    {
        OnPropertyChanged(nameof(TotalAccessCount));
        OnPropertyChanged(nameof(SitesCount));
        OnPropertyChanged(nameof(HighPrivilegeCount));
    }

    private static void OpenFile(string filePath)
    {
        try
        {
            Process.Start(new ProcessStartInfo(filePath) { UseShellExecute = true });
        }
        catch
        {
            // Non-critical: file was written successfully, just can't auto-open
        }
    }
}
