using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Kiota.Abstractions.Authentication;

namespace SharepointToolbox.Infrastructure.Auth;

public class GraphClientFactory
{
    private readonly MsalClientFactory _msalFactory;

    public GraphClientFactory(MsalClientFactory msalFactory)
    {
        _msalFactory = msalFactory;
    }

    /// <summary>
    /// Creates a GraphServiceClient that acquires tokens via the same MSAL PCA
    /// used for SharePoint auth, but with Graph scopes. Uses the /common authority
    /// and the <c>.default</c> scope (whatever the client is pre-consented for).
    /// </summary>
    public Task<GraphServiceClient> CreateClientAsync(string clientId, CancellationToken ct)
        => CreateClientAsync(clientId, tenantId: null, scopes: null, ct);

    /// <summary>
    /// Creates a GraphServiceClient pinned to a specific tenant authority.
    /// Pass the tenant domain (e.g. "contoso.onmicrosoft.com") or tenant GUID.
    /// Null <paramref name="tenantId"/> falls back to /common.
    /// </summary>
    public Task<GraphServiceClient> CreateClientAsync(string clientId, string? tenantId, CancellationToken ct)
        => CreateClientAsync(clientId, tenantId, scopes: null, ct);

    /// <summary>
    /// Creates a GraphServiceClient with explicit Graph delegated scopes.
    /// Use when <c>.default</c> is insufficient — typically for admin actions that
    /// need scopes not pre-consented on the bootstrap client (e.g. app registration
    /// requires <c>Application.ReadWrite.All</c> and
    /// <c>DelegatedPermissionGrant.ReadWrite.All</c>). Triggers an admin-consent
    /// prompt on first use if the tenant has not yet consented.
    /// </summary>
    public async Task<GraphServiceClient> CreateClientAsync(
        string clientId,
        string? tenantId,
        string[]? scopes,
        CancellationToken ct)
    {
        var pca = await _msalFactory.GetOrCreateAsync(clientId);

        // Always reuse a cached account when one exists — `WithTenantId` on the
        // silent/interactive call redirects the authority, and MSAL stores
        // refresh tokens per tenant. Skipping the cached account forces an
        // interactive prompt on every Graph call (the bug that produced 4–5
        // sign-in windows during app registration).
        var accounts = await pca.GetAccountsAsync();
        var account = accounts.FirstOrDefault();

        var graphScopes = scopes ?? new[] { "https://graph.microsoft.com/.default" };

        var tokenProvider = new MsalTokenProvider(pca, account, graphScopes, tenantId);
        var authProvider = new BaseBearerTokenAuthenticationProvider(tokenProvider);
        return new GraphServiceClient(authProvider);
    }
}

/// <summary>
/// Bridges MSAL PCA token acquisition with Graph SDK's IAccessTokenProvider interface.
/// </summary>
internal class MsalTokenProvider : IAccessTokenProvider
{
    private readonly IPublicClientApplication _pca;
    private IAccount? _account;
    private readonly string[] _scopes;
    private readonly string? _tenantId;

    public MsalTokenProvider(IPublicClientApplication pca, IAccount? account, string[] scopes, string? tenantId = null)
    {
        _pca = pca;
        _account = account;
        _scopes = scopes;
        _tenantId = tenantId;
    }

    public AllowedHostsValidator AllowedHostsValidator { get; } = new();

    public async Task<string> GetAuthorizationTokenAsync(
        Uri uri,
        Dictionary<string, object>? additionalAuthenticationContext = null,
        CancellationToken cancellationToken = default)
    {
        // Refresh _account from PCA cache each call — interactive flows on a
        // sibling token provider populate the cache, and we want the next
        // request on this provider to use that account silently.
        if (_account is null)
        {
            var accounts = await _pca.GetAccountsAsync();
            _account = accounts.FirstOrDefault();
        }

        if (_account is not null)
        {
            try
            {
                var silent = _pca.AcquireTokenSilent(_scopes, _account);
                if (_tenantId is not null) silent = silent.WithTenantId(_tenantId);
                var result = await silent.ExecuteAsync(cancellationToken);
                return result.AccessToken;
            }
            catch (MsalUiRequiredException)
            {
                // fall through to interactive
            }
        }

        var interactive = _pca.AcquireTokenInteractive(_scopes);
        if (_tenantId is not null) interactive = interactive.WithTenantId(_tenantId);
        var interactiveResult = await interactive.ExecuteAsync(cancellationToken);
        // Cache the account so subsequent calls on this provider go silent.
        _account = interactiveResult.Account;
        return interactiveResult.AccessToken;
    }
}
