using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.SharePoint.Client;
using Serilog;
using SharepointToolbox.Core.Helpers;
using SharepointToolbox.Core.Models;
using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;
using GraphUser = Microsoft.Graph.Models.User;
using GraphUserCollectionResponse = Microsoft.Graph.Models.UserCollectionResponse;

namespace SharepointToolbox.Services;

/// <summary>
/// CSOM + Microsoft Graph implementation of <see cref="ISharePointGroupResolver"/>.
///
/// Resolution strategy (Phase 17):
///   1. Iterate distinct group names (OrdinalIgnoreCase).
///   2. Per group: load users via CSOM <c>ctx.Web.SiteGroups.GetByName(name).Users</c>.
///   3. Per user: if login matches AAD group pattern (<see cref="IsAadGroup"/>), resolve
///      transitively via Graph <c>groups/{id}/transitiveMembers/microsoft.graph.user</c>.
///   4. De-duplicate leaf members by Login (OrdinalIgnoreCase).
///   5. On any error: log warning and return empty list for that group (never throw).
/// </summary>
public class SharePointGroupResolver : ISharePointGroupResolver
{
    private readonly AppGraphClientFactory _graphClientFactory;

    public SharePointGroupResolver(AppGraphClientFactory graphClientFactory)
    {
        _graphClientFactory = graphClientFactory;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyDictionary<string, IReadOnlyList<ResolvedMember>>> ResolveGroupsAsync(
        ClientContext ctx,
        string clientId,
        IReadOnlyList<string> groupNames,
        CancellationToken ct)
    {
        var result = new Dictionary<string, IReadOnlyList<ResolvedMember>>(
            StringComparer.OrdinalIgnoreCase);

        if (groupNames.Count == 0)
            return result;

        GraphServiceClient? graphClient = null;

        // Preload the web's SiteGroups catalog once, so we can skip missing
        // groups without triggering a server round-trip per name (which fills
        // logs with "Could not resolve SP group" warnings for groups that
        // live on other sites or were renamed/deleted).
        var groupTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        try
        {
            ctx.Load(ctx.Web.SiteGroups, gs => gs.Include(g => g.Title));
            await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);
            foreach (var g in ctx.Web.SiteGroups)
                groupTitles.Add(g.Title);
        }
        catch (OperationCanceledException) { throw; }
        catch (Exception ex)
        {
            Log.Warning("Could not enumerate SiteGroups on {Url}: {Error}", ctx.Url, ex.Message);
        }

        foreach (var groupName in groupNames.Distinct(StringComparer.OrdinalIgnoreCase))
        {
            ct.ThrowIfCancellationRequested();

            if (!groupTitles.Contains(groupName))
            {
                // Group not on this web — likely scoped to another site in a
                // multi-site scan. Keep quiet: log at Debug, return empty.
                Log.Debug("SP group '{Group}' not present on {Url}; skipping.",
                    groupName, ctx.Url);
                result[groupName] = Array.Empty<ResolvedMember>();
                continue;
            }

            try
            {
                var group = ctx.Web.SiteGroups.GetByName(groupName);
                ctx.Load(group.Users, users => users.Include(
                    u => u.Title,
                    u => u.LoginName,
                    u => u.PrincipalType));
                await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);

                var members = new List<ResolvedMember>();

                foreach (var user in group.Users)
                {
                    if (IsAadGroup(user.LoginName))
                    {
                        // Lazy-create graph client on first AAD group encountered
                        graphClient ??= await _graphClientFactory.CreateClientAsync(clientId, ct);

                        var aadId = ExtractAadGroupId(user.LoginName);
                        var leafUsers = await ResolveAadGroupAsync(graphClient, aadId, ct);
                        members.AddRange(leafUsers);
                    }
                    else
                    {
                        members.Add(new ResolvedMember(
                            DisplayName: user.Title ?? user.LoginName,
                            Login: StripClaims(user.LoginName)));
                    }
                }

                result[groupName] = members
                    .DistinctBy(m => m.Login, StringComparer.OrdinalIgnoreCase)
                    .ToList();
            }
            catch (OperationCanceledException) { throw; }
            catch (Exception ex)
            {
                Log.Warning("Could not resolve SP group '{Group}': {Error}", groupName, ex.Message);
                result[groupName] = Array.Empty<ResolvedMember>();
            }
        }

        return result;
    }

    // ── Static helpers (internal for testability via InternalsVisibleTo) ───────

    /// <summary>
    /// Returns <c>true</c> if the login name represents a nested AAD/M365 group
    /// (login prefix pattern <c>c:0t.c|tenant|</c>).
    /// </summary>
    internal static bool IsAadGroup(string login) =>
        login.StartsWith("c:0t.c|", StringComparison.OrdinalIgnoreCase);

    /// <summary>
    /// Extracts the AAD object GUID from an AAD group login name.
    /// e.g. "c:0t.c|tenant|aaaabbbb-cccc-dddd-eeee-ffffgggghhhh" → "aaaabbbb-cccc-dddd-eeee-ffffgggghhhh"
    /// </summary>
    internal static string ExtractAadGroupId(string login) =>
        login[(login.LastIndexOf('|') + 1)..];

    /// <summary>
    /// Strips the SharePoint claims prefix from a login name, returning the UPN or identifier after the last pipe.
    /// e.g. "i:0#.f|membership|user@contoso.com" → "user@contoso.com"
    /// </summary>
    internal static string StripClaims(string login) =>
        login[(login.LastIndexOf('|') + 1)..];

    // ── Private: Graph transitive member resolution ────────────────────────────

    private static async Task<IEnumerable<ResolvedMember>> ResolveAadGroupAsync(
        GraphServiceClient graphClient,
        string aadGroupId,
        CancellationToken ct)
    {
        try
        {
            var response = await graphClient
                .Groups[aadGroupId]
                .TransitiveMembers
                .GraphUser
                .GetAsync(config =>
                {
                    config.QueryParameters.Select = new[] { "displayName", "userPrincipalName" };
                    config.QueryParameters.Top = 999;
                }, ct);

            if (response?.Value is null)
                return Enumerable.Empty<ResolvedMember>();

            var members = new List<ResolvedMember>();

            var pageIterator = PageIterator<GraphUser, GraphUserCollectionResponse>.CreatePageIterator(
                graphClient,
                response,
                user =>
                {
                    if (ct.IsCancellationRequested) return false;
                    members.Add(new ResolvedMember(
                        DisplayName: user.DisplayName ?? user.UserPrincipalName ?? "Unknown",
                        Login: user.UserPrincipalName ?? string.Empty));
                    return true;
                });

            await pageIterator.IterateAsync(ct);
            return members;
        }
        catch (OperationCanceledException) { throw; }
        catch (Exception ex)
        {
            Log.Warning("Could not resolve AAD group '{Id}' transitively: {Error}", aadGroupId, ex.Message);
            return Enumerable.Empty<ResolvedMember>();
        }
    }
}
