using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using SharepointToolbox.Core.Models;
using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;
using AppMsalClientFactory = SharepointToolbox.Infrastructure.Auth.MsalClientFactory;

namespace SharepointToolbox.Services;

/// <summary>
/// Manages Azure AD app registration and removal using the Microsoft Graph API.
/// All operations use <see cref="GraphClientFactory"/> for token acquisition.
/// </summary>
/// <remarks>
/// <para>GraphServiceClient lifecycle: a fresh client is created per public call
/// (<see cref="IsGlobalAdminAsync"/>, <see cref="RegisterAsync"/>,
/// <see cref="RollbackAsync"/>, <see cref="RemoveAsync"/>). This is intentional —
/// each call may use different scopes (RegistrationScopes vs. default) and target
/// a different tenant, so a cached per-service instance would bind the wrong
/// authority. The factory itself caches the underlying MSAL PCA and token cache,
/// so client construction is cheap (no network hit when tokens are valid).</para>
/// <para>Do not cache a GraphServiceClient at call sites — always go through
/// <see cref="GraphClientFactory"/> so tenant pinning and scope selection stay
/// correct.</para>
/// </remarks>
public class AppRegistrationService : IAppRegistrationService
{
    // Entra built-in directory role template IDs are global constants shared across all tenants.
    // GlobalAdminTemplateId: "Global Administrator" directoryRoleTemplate.
    // See https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference#global-administrator
    private const string GlobalAdminTemplateId = "62e90394-69f5-4237-9190-012177145e10";

    // First-party Microsoft service appIds (constant across tenants).
    private const string GraphAppId = "00000003-0000-0000-c000-000000000000";
    private const string SharePointAppId = "00000003-0000-0ff1-ce00-000000000000";

    // Explicit scopes for the registration flow. The bootstrap client
    // (Microsoft Graph Command Line Tools) does not pre-consent these, so
    // requesting `.default` returns a token without them → POST /applications
    // fails with 403 even for a Global Admin. Requesting them explicitly
    // triggers the admin-consent prompt on first use.
    private static readonly string[] RegistrationScopes = new[]
    {
        "https://graph.microsoft.com/Application.ReadWrite.All",
        "https://graph.microsoft.com/DelegatedPermissionGrant.ReadWrite.All",
        "https://graph.microsoft.com/Directory.Read.All",
    };

    private readonly AppGraphClientFactory _graphFactory;
    private readonly AppMsalClientFactory _msalFactory;
    private readonly ISessionManager _sessionManager;
    private readonly ILogger<AppRegistrationService> _logger;

    public AppRegistrationService(
        AppGraphClientFactory graphFactory,
        AppMsalClientFactory msalFactory,
        ISessionManager sessionManager,
        ILogger<AppRegistrationService> logger)
    {
        _graphFactory = graphFactory;
        _msalFactory = msalFactory;
        _sessionManager = sessionManager;
        _logger = logger;
    }

    /// <inheritdoc/>
    public async Task<bool> IsGlobalAdminAsync(string clientId, string tenantUrl, CancellationToken ct)
    {
        // No $filter: isof() on directoryObject requires advanced query params
        // (ConsistencyLevel: eventual + $count=true) and fails with 400 otherwise.
        // The user's membership list is small; filtering client-side is fine.
        var tenantId = ResolveTenantId(tenantUrl);
        var graphClient = await _graphFactory.CreateClientAsync(clientId, tenantId, ct);
        var memberships = await graphClient.Me.TransitiveMemberOf.GetAsync(cancellationToken: ct);

        return memberships?.Value?
            .OfType<DirectoryRole>()
            .Any(r => string.Equals(r.RoleTemplateId, GlobalAdminTemplateId,
                StringComparison.OrdinalIgnoreCase)) ?? false;
    }

    /// <inheritdoc/>
    public async Task<AppRegistrationResult> RegisterAsync(
        string clientId,
        string tenantUrl,
        string tenantDisplayName,
        CancellationToken ct)
    {
        var tenantId = ResolveTenantId(tenantUrl);
        Application? createdApp = null;
        try
        {
            var graphClient = await _graphFactory.CreateClientAsync(clientId, tenantId, RegistrationScopes, ct);

            // Step 1: Create Application object
            var appRequest = new Application
            {
                DisplayName = $"SharePoint Toolbox - {tenantDisplayName}",
                SignInAudience = "AzureADMyOrg",
                IsFallbackPublicClient = true,
                PublicClient = new PublicClientApplication
                {
                    RedirectUris = new List<string>
                    {
                        // Loopback URI for MSAL desktop default (any port accepted by Entra).
                        "http://localhost",
                        // Legacy native-client URI for embedded WebView fallback.
                        "https://login.microsoftonline.com/common/oauth2/nativeclient",
                    }
                },
                RequiredResourceAccess = BuildRequiredResourceAccess()
            };

            createdApp = await graphClient.Applications.PostAsync(appRequest, cancellationToken: ct);
            _logger.LogInformation("Created Application {AppId} ({ObjectId})", createdApp!.AppId, createdApp.Id);

            // Step 2: Create ServicePrincipal
            var sp = await graphClient.ServicePrincipals.PostAsync(
                new ServicePrincipal { AppId = createdApp.AppId }, cancellationToken: ct);
            _logger.LogInformation("Created ServicePrincipal {SpId}", sp!.Id);

            // Step 3a: Look up Microsoft Graph resource SP
            var graphSpCollection = await graphClient.ServicePrincipals.GetAsync(req =>
            {
                req.QueryParameters.Filter = $"appId eq '{GraphAppId}'";
                req.QueryParameters.Select = new[] { "id" };
            }, ct);
            var graphResourceId = graphSpCollection?.Value?.FirstOrDefault()?.Id
                ?? throw new InvalidOperationException("Microsoft Graph service principal not found.");

            // Step 3b: Look up SharePoint Online resource SP
            var spoSpCollection = await graphClient.ServicePrincipals.GetAsync(req =>
            {
                req.QueryParameters.Filter = $"appId eq '{SharePointAppId}'";
                req.QueryParameters.Select = new[] { "id" };
            }, ct);
            var spoResourceId = spoSpCollection?.Value?.FirstOrDefault()?.Id
                ?? throw new InvalidOperationException("SharePoint Online service principal not found.");

            // Step 4a: Grant delegated permissions for Microsoft Graph
            await graphClient.Oauth2PermissionGrants.PostAsync(new OAuth2PermissionGrant
            {
                ClientId = sp.Id,
                ConsentType = "AllPrincipals",
                ResourceId = graphResourceId,
                Scope = "User.Read User.Read.All Group.Read.All Directory.Read.All"
            }, cancellationToken: ct);

            // Step 4b: Grant delegated permissions for SharePoint Online
            await graphClient.Oauth2PermissionGrants.PostAsync(new OAuth2PermissionGrant
            {
                ClientId = sp.Id,
                ConsentType = "AllPrincipals",
                ResourceId = spoResourceId,
                Scope = "AllSites.FullControl"
            }, cancellationToken: ct);

            _logger.LogInformation("App registration complete. AppId={AppId}", createdApp.AppId);
            return AppRegistrationResult.Success(createdApp.AppId!);
        }
        catch (Microsoft.Graph.Models.ODataErrors.ODataError odataEx)
            when (odataEx.ResponseStatusCode == 401 || odataEx.ResponseStatusCode == 403)
        {
            _logger.LogWarning(odataEx,
                "RegisterAsync refused by Graph (status {Status}) — user lacks role/consent. Surfacing fallback.",
                odataEx.ResponseStatusCode);
            await RollbackAsync(createdApp, clientId, tenantId, ct);
            return AppRegistrationResult.FallbackRequired();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "RegisterAsync failed. Attempting rollback.");
            await RollbackAsync(createdApp, clientId, tenantId, ct);
            return AppRegistrationResult.Failure(ex.Message);
        }
    }

    private async Task RollbackAsync(Application? createdApp, string clientId, string tenantId, CancellationToken ct)
    {
        if (createdApp?.Id is null) return;
        try
        {
            var graphClient = await _graphFactory.CreateClientAsync(clientId, tenantId, RegistrationScopes, ct);
            await graphClient.Applications[createdApp.Id].DeleteAsync(cancellationToken: ct);
            _logger.LogInformation("Rollback: deleted Application {ObjectId}", createdApp.Id);
        }
        catch (Exception rollbackEx)
        {
            _logger.LogWarning(rollbackEx, "Rollback failed for Application {ObjectId}", createdApp.Id);
        }
    }

    /// <inheritdoc/>
    public async Task RemoveAsync(string clientId, string tenantUrl, string appId, CancellationToken ct)
    {
        try
        {
            var tenantId = ResolveTenantId(tenantUrl);
            var graphClient = await _graphFactory.CreateClientAsync(clientId, tenantId, RegistrationScopes, ct);
            await graphClient.Applications[$"(appId='{appId}')"].DeleteAsync(cancellationToken: ct);
            _logger.LogInformation("Removed Application appId={AppId}", appId);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "RemoveAsync failed for appId={AppId}", appId);
        }
    }

    /// <summary>
    /// Derives a tenant identifier (domain) from a SharePoint tenant URL so MSAL
    /// can pin the authority to the correct tenant. Examples:
    ///   https://contoso.sharepoint.com       → contoso.onmicrosoft.com
    ///   https://contoso-admin.sharepoint.com → contoso.onmicrosoft.com
    /// Throws <see cref="ArgumentException"/> when the URL is not a recognisable
    /// SharePoint URL — falling back to /common would silently route registration
    /// to the signed-in user's home tenant, which is the bug this guards against.
    /// </summary>
    internal static string ResolveTenantId(string tenantUrl)
    {
        if (!Uri.TryCreate(tenantUrl, UriKind.Absolute, out var uri))
            throw new ArgumentException($"Invalid tenant URL: '{tenantUrl}'", nameof(tenantUrl));

        var host = uri.Host;
        var firstDot = host.IndexOf('.');
        if (firstDot <= 0)
            throw new ArgumentException($"Cannot derive tenant from host '{host}'", nameof(tenantUrl));

        var prefix = host.Substring(0, firstDot);
        if (prefix.EndsWith("-admin", StringComparison.OrdinalIgnoreCase))
            prefix = prefix.Substring(0, prefix.Length - "-admin".Length);

        return $"{prefix}.onmicrosoft.com";
    }

    /// <inheritdoc/>
    public async Task ClearMsalSessionAsync(string clientId, string tenantUrl)
    {
        // 1. Clear live ClientContext from SessionManager
        await _sessionManager.ClearSessionAsync(tenantUrl);

        // 2. Clear in-memory MSAL accounts
        var pca = await _msalFactory.GetOrCreateAsync(clientId);
        var accounts = (await pca.GetAccountsAsync()).ToList();
        while (accounts.Any())
        {
            await pca.RemoveAsync(accounts.First());
            accounts = (await pca.GetAccountsAsync()).ToList();
        }

        // 3. Unregister persistent cache (releases file lock, harmless if not registered)
        var helper = _msalFactory.GetCacheHelper(clientId);
        helper.UnregisterCache(pca.UserTokenCache);

        _logger.LogInformation("MSAL session cleared for clientId={ClientId}", clientId);
    }

    /// <summary>
    /// Builds the RequiredResourceAccess list for the app registration manifest.
    /// Declares delegated permissions for Microsoft Graph and SharePoint Online.
    /// </summary>
    /// <remarks>
    /// Graph permission GUIDs are HIGH confidence (stable, from official Microsoft docs).
    /// SharePoint AllSites.FullControl GUID (56680e0d-d2a3-4ae1-80d8-3c4a5c70c4a6) is LOW confidence
    /// and should be verified against a live tenant by querying:
    /// GET /servicePrincipals?$filter=appId eq '00000003-0000-0ff1-ce00-000000000000'&amp;$select=oauth2PermissionScopes
    /// </remarks>
    internal static List<RequiredResourceAccess> BuildRequiredResourceAccess()
    {
        var graphAccess = new RequiredResourceAccess
        {
            ResourceAppId = GraphAppId,
            ResourceAccess = new List<ResourceAccess>
            {
                new() { Id = Guid.Parse("e1fe6dd8-ba31-4d61-89e7-88639da4683d"), Type = "Scope" }, // User.Read
                new() { Id = Guid.Parse("a154be20-db9c-4678-8ab7-66f6cc099a59"), Type = "Scope" }, // User.Read.All (delegated)
                new() { Id = Guid.Parse("5b567255-7703-4780-807c-7be8301ae99b"), Type = "Scope" }, // Group.Read.All (delegated)
                new() { Id = Guid.Parse("06da0dbc-49e2-44d2-8312-53f166ab848a"), Type = "Scope" }, // Directory.Read.All (delegated)
            }
        };

        // LOW confidence GUID — verify against live tenant before deploying
        var spoAccess = new RequiredResourceAccess
        {
            ResourceAppId = SharePointAppId,
            ResourceAccess = new List<ResourceAccess>
            {
                new() { Id = Guid.Parse("56680e0d-d2a3-4ae1-80d8-3c4a5c70c4a6"), Type = "Scope" }, // AllSites.FullControl (delegated) — LOW confidence GUID
            }
        };

        return new List<RequiredResourceAccess> { graphAccess, spoAccess };
    }
}
