using Microsoft.SharePoint.Client;
using Serilog;
using SharepointToolbox.Core.Helpers;
using SharepointToolbox.Core.Models;

namespace SharepointToolbox.Services;

/// <summary>
/// CSOM scan engine — faithful C# port of the PowerShell Generate-PnPSitePermissionRpt
/// and Get-PnPPermissions functions (PS reference lines 1361-1989).
/// </summary>
public class PermissionsService : IPermissionsService
{
    private readonly ISystemGroupTargetResolver? _systemGroupResolver;

    public PermissionsService() : this(null) { }

    public PermissionsService(ISystemGroupTargetResolver? systemGroupResolver)
    {
        _systemGroupResolver = systemGroupResolver;
    }


    /// <summary>
    /// Detects the SharePoint server error raised when a RoleAssignment member
    /// refers to a user that no longer resolves (orphaned Azure AD account).
    /// Message surfaces in the user's locale — match on language-agnostic tokens.
    /// </summary>
    private static bool IsClaimsResolutionError(ServerException ex)
    {
        var msg = ex.Message ?? string.Empty;
        return msg.Contains("Claims", StringComparison.OrdinalIgnoreCase)
            || msg.Contains("Revendications", StringComparison.OrdinalIgnoreCase)
            || msg.Contains("Org ID", StringComparison.OrdinalIgnoreCase)
            || msg.Contains("ID org", StringComparison.OrdinalIgnoreCase)
            || msg.Contains("OrgIdToClaims", StringComparison.OrdinalIgnoreCase);
    }

    // Port of PS lines 1914-1926: system lists excluded from permission reporting
    private static readonly HashSet<string> ExcludedLists = new(StringComparer.OrdinalIgnoreCase)
    {
        "Access Requests", "App Packages", "appdata", "appfiles", "Apps in Testing",
        "Cache Profiles", "Composed Looks", "Content and Structure Reports",
        "Content type publishing error log", "Converted Forms", "Device Channels",
        "Form Templates", "fpdatasources", "List Template Gallery",
        "Long Running Operation Status", "Maintenance Log Library", "Images",
        "site collection images", "Master Docs", "Master Page Gallery", "MicroFeed",
        "NintexFormXml", "Quick Deploy Items", "Relationships List", "Reusable Content",
        "Reporting Metadata", "Reporting Templates", "Search Config List", "Site Assets",
        "Preservation Hold Library", "Site Pages", "Solution Gallery", "Style Library",
        "Suggested Content Browser Locations", "Theme Gallery", "TaxonomyHiddenList",
        "User Information List", "Web Part Gallery", "wfpub", "wfsvc",
        "Workflow History", "Workflow Tasks", "Pages"
    };

    public async Task<IReadOnlyList<PermissionEntry>> ScanSiteAsync(
        ClientContext ctx,
        ScanOptions options,
        IProgress<OperationProgress> progress,
        CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();

        var results = new List<PermissionEntry>();

        // 1. Site collection administrators
        progress.Report(OperationProgress.Indeterminate("Scanning site collection admins…"));
        results.AddRange(await GetSiteCollectionAdminsAsync(ctx, progress, ct));

        // 2. Web-level permissions
        ctx.Load(ctx.Web,
            w => w.Title,
            w => w.Url,
            w => w.Lists.Include(
                l => l.Title, l => l.DefaultViewUrl, l => l.Hidden,
                l => l.BaseType, l => l.IsSystemList),
            w => w.Webs.Include(sw => sw.Title, sw => sw.Url));
        await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);

        progress.Report(OperationProgress.Indeterminate($"Scanning web: {ctx.Web.Url}…"));
        results.AddRange(await GetWebPermissionsAsync(ctx, ctx.Web, options, progress, ct));

        // 3. Lists and libraries
        foreach (var list in ctx.Web.Lists)
        {
            ct.ThrowIfCancellationRequested();
            if (list.Hidden || list.IsSystemList || ExcludedLists.Contains(list.Title))
                continue;

            progress.Report(OperationProgress.Indeterminate($"Scanning list: {list.Title}…"));
            results.AddRange(await GetListPermissionsAsync(ctx, list, options, progress, ct));

            // 4. Folders (if configured)
            if (options.ScanFolders && list.BaseType == BaseType.DocumentLibrary)
                results.AddRange(await GetFolderPermissionsAsync(ctx, list, options, progress, ct));
        }

        // 5. Subsites (if configured)
        if (options.IncludeSubsites)
        {
            foreach (var subweb in ctx.Web.Webs)
            {
                ct.ThrowIfCancellationRequested();
                using var subCtx = ctx.Clone(subweb.Url);
                subCtx.Load(subCtx.Web,
                    w => w.Title,
                    w => w.Url,
                    w => w.Lists.Include(
                        l => l.Title, l => l.DefaultViewUrl, l => l.Hidden,
                        l => l.BaseType, l => l.IsSystemList),
                    w => w.Webs.Include(sw => sw.Title, sw => sw.Url));
                await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(subCtx, progress, ct);

                progress.Report(OperationProgress.Indeterminate($"Scanning subsite: {subweb.Url}…"));
                results.AddRange(await GetWebPermissionsAsync(subCtx, subCtx.Web, options, progress, ct));

                foreach (var list in subCtx.Web.Lists)
                {
                    ct.ThrowIfCancellationRequested();
                    if (list.Hidden || list.IsSystemList || ExcludedLists.Contains(list.Title))
                        continue;

                    results.AddRange(await GetListPermissionsAsync(subCtx, list, options, progress, ct));

                    if (options.ScanFolders && list.BaseType == BaseType.DocumentLibrary)
                        results.AddRange(await GetFolderPermissionsAsync(subCtx, list, options, progress, ct));
                }
            }
        }

        return results;
    }

    /// <summary>
    /// Returns a single PermissionEntry for site collection administrators as a group.
    /// Port of PS lines 1361-1400: Get site collection admins via SiteUsers filter.
    /// </summary>
    private async Task<IEnumerable<PermissionEntry>> GetSiteCollectionAdminsAsync(
        ClientContext ctx,
        IProgress<OperationProgress> progress,
        CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();

        ctx.Load(ctx.Web, w => w.Url, w => w.Title);
        ctx.Load(ctx.Web.SiteUsers,
            users => users.Include(
                u => u.Title,
                u => u.LoginName,
                u => u.IsSiteAdmin));

        try
        {
            await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
        }
        catch (ServerException ex) when (IsClaimsResolutionError(ex))
        {
            Log.Warning("Skipped site collection admins for {Url} — orphaned user: {Error}",
                ctx.Web.Url, ex.Message);
            return Enumerable.Empty<PermissionEntry>();
        }

        var admins = ctx.Web.SiteUsers
            .Where(u => u.IsSiteAdmin)
            .ToList();

        if (admins.Count == 0)
            return Enumerable.Empty<PermissionEntry>();

        var users = string.Join(";", admins.Select(u => u.Title));
        var logins = string.Join(";", admins.Select(u => u.LoginName));

        return new[]
        {
            new PermissionEntry(
                ObjectType: "Site Collection",
                Title: ctx.Web.Title,
                Url: ctx.Web.Url,
                HasUniquePermissions: true,
                Users: users,
                UserLogins: logins,
                PermissionLevels: "Site Collection Administrator",
                GrantedThrough: "Direct Permissions",
                PrincipalType: "User")
        };
    }

    /// <summary>
    /// Returns permission entries for a Web object.
    /// Port of PS Get-PnPPermissions for Web objects.
    /// </summary>
    private async Task<IEnumerable<PermissionEntry>> GetWebPermissionsAsync(
        ClientContext ctx,
        Web web,
        ScanOptions options,
        IProgress<OperationProgress> progress,
        CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        return await ExtractPermissionsAsync(ctx, web, "Site", web.Title, web.Url, options, progress, ct);
    }

    /// <summary>
    /// Returns permission entries for a List object.
    /// Port of PS Get-PnPPermissions for List objects.
    /// </summary>
    private async Task<IEnumerable<PermissionEntry>> GetListPermissionsAsync(
        ClientContext ctx,
        List list,
        ScanOptions options,
        IProgress<OperationProgress> progress,
        CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();

        // Build the list URL from DefaultViewUrl
        var listUrl = list.DefaultViewUrl;
        if (!string.IsNullOrEmpty(listUrl))
        {
            var uri = new Uri(ctx.Url);
            listUrl = $"{uri.Scheme}://{uri.Host}{listUrl}";
        }

        return await ExtractPermissionsAsync(ctx, list, "List", list.Title, listUrl ?? ctx.Url, options, progress, ct);
    }

    /// <summary>
    /// Enumerates folders in a document library and returns permission entries per folder.
    /// Uses SharePointPaginationHelper.GetAllItemsAsync — never raw list enumeration.
    /// </summary>
    private async Task<IEnumerable<PermissionEntry>> GetFolderPermissionsAsync(
        ClientContext ctx,
        List list,
        ScanOptions options,
        IProgress<OperationProgress> progress,
        CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();

        var results = new List<PermissionEntry>();

        // CAML query for all folders, ordered by ID
        var camlQuery = new CamlQuery
        {
            ViewXml = @"<View Scope='RecursiveAll'>
                            <Query><OrderBy><FieldRef Name='ID'/></OrderBy></Query>
                            <RowLimit>500</RowLimit>
                        </View>"
        };

        // Calculate base path depth to enforce FolderDepth limit
        ctx.Load(list, l => l.RootFolder.ServerRelativeUrl);
        await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);

        var rootUrl = list.RootFolder.ServerRelativeUrl.TrimEnd('/');
        var rootDepth = rootUrl.Split('/').Length;

        await foreach (var item in SharePointPaginationHelper.GetAllItemsAsync(ctx, list, camlQuery, ct))
        {
            ct.ThrowIfCancellationRequested();

            if (item.FileSystemObjectType != FileSystemObjectType.Folder)
                continue;

            var fileRef = item["FileRef"]?.ToString() ?? string.Empty;
            if (string.IsNullOrEmpty(fileRef))
                continue;

            // Check depth constraint
            if (options.FolderDepth != 999)
            {
                var folderDepth = fileRef.TrimEnd('/').Split('/').Length - rootDepth;
                if (folderDepth > options.FolderDepth)
                    continue;
            }

            // ListItem is a SecurableObject; use it directly for permission extraction.
            // Load the backing folder for URL/name metadata only.
            var folder = item.Folder;
            ctx.Load(folder, f => f.ServerRelativeUrl, f => f.Name);
            await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);

            var uri = new Uri(ctx.Url);
            var folderUrl = $"{uri.Scheme}://{uri.Host}{folder.ServerRelativeUrl}";

            var folderEntries = await ExtractPermissionsAsync(
                ctx, item, "Folder", folder.Name, folderUrl, options, progress, ct);
            results.AddRange(folderEntries);
        }

        return results;
    }

    /// <summary>
    /// Core per-object permission extractor.
    /// Batches CSOM Load + ExecuteQueryRetryAsync into one round-trip per object.
    /// Port of PS Get-PnPPermissions inner logic.
    /// </summary>
    private async Task<IEnumerable<PermissionEntry>> ExtractPermissionsAsync(
        ClientContext ctx,
        SecurableObject obj,
        string objectType,
        string title,
        string url,
        ScanOptions options,
        IProgress<OperationProgress> progress,
        CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();

        // Batched load: one CSOM round-trip for all fields we need
        ctx.Load(obj,
            o => o.HasUniqueRoleAssignments,
            o => o.RoleAssignments.Include(
                ra => ra.Member.Title,
                ra => ra.Member.LoginName,
                ra => ra.Member.PrincipalType,
                ra => ra.RoleDefinitionBindings.Include(rdb => rdb.Name)));

        // Orphaned AD users in RoleAssignments cause the server to throw
        // "Cannot convert Org ID user to Claims user" during claim resolution.
        // That kills the whole batch — skip this object so the scan continues.
        // Only swallow the claims-resolution signature; real access-denied errors
        // must bubble up so callers (e.g. PermissionsViewModel auto-elevation)
        // can react to them.
        try
        {
            await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
        }
        catch (ServerException ex) when (IsClaimsResolutionError(ex))
        {
            Log.Warning("Skipped {Type} '{Title}' ({Url}) — orphaned user in permissions: {Error}",
                objectType, title, url, ex.Message);
            return Enumerable.Empty<PermissionEntry>();
        }

        // Skip inherited objects when IncludeInherited=false
        if (!options.IncludeInherited && !obj.HasUniqueRoleAssignments)
            return Enumerable.Empty<PermissionEntry>();

        var entries = new List<PermissionEntry>();

        foreach (var ra in obj.RoleAssignments)
        {
            ct.ThrowIfCancellationRequested();

            var member = ra.Member;
            var loginName = member.LoginName ?? string.Empty;
            var memberTitle = member.Title ?? string.Empty;

            // Classify the member name. The bare "Limited Access System Group" is
            // pure noise; drop it. The For-Web/For-List and SharingLinks variants
            // are kept and enriched below with a resolved target URL.
            var classification = PermissionEntryHelper.Classify(memberTitle);
            if (classification.Kind == SystemGroupKind.None
                && PermissionEntryHelper.IsBareLimitedAccessSystemGroup(loginName))
            {
                continue;
            }
            if (classification.Kind == SystemGroupKind.LimitedAccessBare)
                continue;

            // Collect and filter permission levels
            var rawLevels = ra.RoleDefinitionBindings.Select(rdb => rdb.Name);
            var filteredLevels = PermissionEntryHelper.FilterPermissionLevels(rawLevels);

            // Drop the row entirely if all levels are removed (e.g., only had "Limited Access")
            if (filteredLevels.Count == 0)
                continue;

            var permLevels = string.Join(";", filteredLevels);

            // Determine principal type
            string principalType;
            if (PermissionEntryHelper.IsExternalUser(loginName))
                principalType = "External User";
            else if (member.PrincipalType == Microsoft.SharePoint.Client.Utilities.PrincipalType.SharePointGroup)
                principalType = "SharePointGroup";
            else
                principalType = "User";

            // Determine how the permission was granted
            string grantedThrough = principalType == "SharePointGroup"
                ? $"SharePoint Group: {memberTitle}"
                : "Direct Permissions";

            // Resolve system-group target (Limited Access For Web/List, SharingLinks)
            string? targetUrl = null;
            string? targetLabel = null;
            string? sharingLinkType = null;
            if (_systemGroupResolver is not null && classification.Kind != SystemGroupKind.None)
            {
                var target = await _systemGroupResolver.ResolveAsync(ctx, classification, ct);
                if (target is not null)
                {
                    targetUrl = target.Url;
                    targetLabel = target.Label;
                    sharingLinkType = target.LinkType;
                }
                else if (classification.Kind == SystemGroupKind.SharingLink)
                {
                    // Target lookup failed (deleted item / no access) — still surface link type.
                    sharingLinkType = classification.LinkType;
                }
            }

            entries.Add(new PermissionEntry(
                ObjectType: objectType,
                Title: title,
                Url: url,
                HasUniquePermissions: obj.HasUniqueRoleAssignments,
                Users: memberTitle,
                UserLogins: loginName,
                PermissionLevels: permLevels,
                GrantedThrough: grantedThrough,
                PrincipalType: principalType,
                TargetUrl: targetUrl,
                TargetLabel: targetLabel,
                SharingLinkType: sharingLinkType));
        }

        return entries;
    }
}
