using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using OpenIddict.Abstractions;
using OpenIddict.Extensions;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
using StellaOps.Auth.Abstractions;
using StellaOps.Authority.OpenIddict;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Stores;

namespace StellaOps.Authority.OpenIddict.Handlers;

internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandler<OpenIddictServerEvents.ValidateTokenRequestContext>
{
    private readonly IAuthorityClientStore clientStore;
    private readonly IAuthorityIdentityProviderRegistry registry;

    public ValidateClientCredentialsHandler(
        IAuthorityClientStore clientStore,
        IAuthorityIdentityProviderRegistry registry)
    {
        this.clientStore = clientStore ?? throw new ArgumentNullException(nameof(clientStore));
        this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
    }

    public async ValueTask HandleAsync(OpenIddictServerEvents.ValidateTokenRequestContext context)
    {
        ArgumentNullException.ThrowIfNull(context);

        if (!context.Request.IsClientCredentialsGrantType())
        {
            return;
        }

        if (string.IsNullOrWhiteSpace(context.ClientId))
        {
            context.Reject(OpenIddictConstants.Errors.InvalidClient, "Client identifier is required.");
            return;
        }

        var document = await clientStore.FindByClientIdAsync(context.ClientId, context.CancellationToken).ConfigureAwait(false);
        if (document is null || document.Disabled)
        {
            context.Reject(OpenIddictConstants.Errors.InvalidClient, "Unknown or disabled client identifier.");
            return;
        }

        IIdentityProviderPlugin? provider = null;
        if (!string.IsNullOrWhiteSpace(document.Plugin))
        {
            if (!registry.TryGet(document.Plugin, out provider))
            {
                context.Reject(OpenIddictConstants.Errors.InvalidClient, "Configured identity provider is unavailable.");
                return;
            }

            if (!provider.Capabilities.SupportsClientProvisioning || provider.ClientProvisioning is null)
            {
                context.Reject(OpenIddictConstants.Errors.UnauthorizedClient, "Associated identity provider does not support client provisioning.");
                return;
            }
        }

        var allowedGrantTypes = ClientCredentialHandlerHelpers.Split(document.Properties, AuthorityClientMetadataKeys.AllowedGrantTypes);
        if (allowedGrantTypes.Count > 0 &&
            !allowedGrantTypes.Any(static grant => string.Equals(grant, OpenIddictConstants.GrantTypes.ClientCredentials, StringComparison.Ordinal)))
        {
            context.Reject(OpenIddictConstants.Errors.UnauthorizedClient, "Client credentials grant is not permitted for this client.");
            return;
        }

        var requiresSecret = string.Equals(document.ClientType, "confidential", StringComparison.OrdinalIgnoreCase);
        if (requiresSecret)
        {
            if (string.IsNullOrWhiteSpace(document.SecretHash))
            {
                context.Reject(OpenIddictConstants.Errors.InvalidClient, "Client secret is not configured.");
                return;
            }

            if (string.IsNullOrWhiteSpace(context.ClientSecret) ||
                !ClientCredentialHandlerHelpers.VerifySecret(context.ClientSecret, document.SecretHash))
            {
                context.Reject(OpenIddictConstants.Errors.InvalidClient, "Invalid client credentials.");
                return;
            }
        }
        else if (!string.IsNullOrWhiteSpace(context.ClientSecret) && !string.IsNullOrWhiteSpace(document.SecretHash) &&
                 !ClientCredentialHandlerHelpers.VerifySecret(context.ClientSecret, document.SecretHash))
        {
            context.Reject(OpenIddictConstants.Errors.InvalidClient, "Invalid client credentials.");
            return;
        }

        var allowedScopes = ClientCredentialHandlerHelpers.Split(document.Properties, AuthorityClientMetadataKeys.AllowedScopes);
        var resolvedScopes = ClientCredentialHandlerHelpers.ResolveGrantedScopes(
            allowedScopes,
            context.Request.GetScopes());

        if (resolvedScopes.InvalidScope is not null)
        {
            context.Reject(OpenIddictConstants.Errors.InvalidScope, $"Scope '{resolvedScopes.InvalidScope}' is not allowed for this client.");
            return;
        }

        context.Transaction.Properties[AuthorityOpenIddictConstants.ClientTransactionProperty] = document;
        if (provider is not null)
        {
            context.Transaction.Properties[AuthorityOpenIddictConstants.ClientProviderTransactionProperty] = provider.Name;
        }

        context.Transaction.Properties[AuthorityOpenIddictConstants.ClientGrantedScopesProperty] = resolvedScopes.Scopes;
    }
}

internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler<OpenIddictServerEvents.HandleTokenRequestContext>
{
    private readonly IAuthorityIdentityProviderRegistry registry;
    private readonly IAuthorityTokenStore tokenStore;
    private readonly TimeProvider clock;

    public HandleClientCredentialsHandler(
        IAuthorityIdentityProviderRegistry registry,
        IAuthorityTokenStore tokenStore,
        TimeProvider clock)
    {
        this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
        this.tokenStore = tokenStore ?? throw new ArgumentNullException(nameof(tokenStore));
        this.clock = clock ?? throw new ArgumentNullException(nameof(clock));
    }

    public async ValueTask HandleAsync(OpenIddictServerEvents.HandleTokenRequestContext context)
    {
        ArgumentNullException.ThrowIfNull(context);

        if (!context.Request.IsClientCredentialsGrantType())
        {
            return;
        }

        if (!context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.ClientTransactionProperty, out var value) ||
            value is not AuthorityClientDocument document)
        {
            context.Reject(OpenIddictConstants.Errors.ServerError, "Client metadata not available.");
            return;
        }

        var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        identity.AddClaim(new Claim(OpenIddictConstants.Claims.Subject, document.ClientId));
        identity.AddClaim(new Claim(OpenIddictConstants.Claims.ClientId, document.ClientId));

        var tokenId = identity.GetClaim(OpenIddictConstants.Claims.JwtId);
        if (string.IsNullOrEmpty(tokenId))
        {
            tokenId = Guid.NewGuid().ToString("N");
            identity.SetClaim(OpenIddictConstants.Claims.JwtId, tokenId);
        }

        identity.SetDestinations(static claim => claim.Type switch
        {
            OpenIddictConstants.Claims.Subject => new[] { OpenIddictConstants.Destinations.AccessToken },
            OpenIddictConstants.Claims.ClientId => new[] { OpenIddictConstants.Destinations.AccessToken },
            OpenIddictConstants.Claims.JwtId => new[] { OpenIddictConstants.Destinations.AccessToken },
            StellaOpsClaimTypes.IdentityProvider => new[] { OpenIddictConstants.Destinations.AccessToken },
            _ => new[] { OpenIddictConstants.Destinations.AccessToken }
        });

        var (provider, descriptor) = await ResolveProviderAsync(context, document).ConfigureAwait(false);
        if (context.IsRejected)
        {
            return;
        }

        if (provider is null)
        {
            if (!string.IsNullOrWhiteSpace(document.Plugin))
            {
                identity.SetClaim(StellaOpsClaimTypes.IdentityProvider, document.Plugin);
            }
        }
        else
        {
            identity.SetClaim(StellaOpsClaimTypes.IdentityProvider, provider.Name);
        }

        var principal = new ClaimsPrincipal(identity);

        var grantedScopes = context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.ClientGrantedScopesProperty, out var scopesValue) &&
                            scopesValue is IReadOnlyList<string> resolvedScopes
            ? resolvedScopes
            : ClientCredentialHandlerHelpers.Split(document.Properties, AuthorityClientMetadataKeys.AllowedScopes);

        if (grantedScopes.Count > 0)
        {
            principal.SetScopes(grantedScopes);
        }
        else
        {
            principal.SetScopes(Array.Empty<string>());
        }

        if (provider is not null && descriptor is not null)
        {
            var enrichmentContext = new AuthorityClaimsEnrichmentContext(provider.Context, user: null, descriptor);
            await provider.ClaimsEnricher.EnrichAsync(identity, enrichmentContext, context.CancellationToken).ConfigureAwait(false);
        }

        await PersistTokenAsync(context, document, tokenId, grantedScopes).ConfigureAwait(false);

        context.Principal = principal;
        context.HandleRequest();
    }

    private async ValueTask<(IIdentityProviderPlugin? Provider, AuthorityClientDescriptor? Descriptor)> ResolveProviderAsync(
        OpenIddictServerEvents.HandleTokenRequestContext context,
        AuthorityClientDocument document)
    {
        string? providerName = null;
        if (context.Transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.ClientProviderTransactionProperty, out var providerValue) &&
            providerValue is string storedProvider)
        {
            providerName = storedProvider;
        }
        else if (!string.IsNullOrWhiteSpace(document.Plugin))
        {
            providerName = document.Plugin;
        }

        if (string.IsNullOrWhiteSpace(providerName))
        {
            return (null, null);
        }

        if (!registry.TryGet(providerName, out var provider) || provider.ClientProvisioning is null)
        {
            context.Reject(OpenIddictConstants.Errors.InvalidClient, "Configured identity provider is unavailable.");
            return (null, null);
        }

        var descriptor = await provider.ClientProvisioning.FindByClientIdAsync(document.ClientId, context.CancellationToken).ConfigureAwait(false);

        if (descriptor is null)
        {
            context.Reject(OpenIddictConstants.Errors.InvalidClient, "Client registration was not found.");
            return (null, null);
        }

        return (provider, descriptor);
    }

    private async ValueTask PersistTokenAsync(
        OpenIddictServerEvents.HandleTokenRequestContext context,
        AuthorityClientDocument document,
        string tokenId,
        IReadOnlyCollection<string> scopes)
    {
        if (context.IsRejected)
        {
            return;
        }

        var issuedAt = clock.GetUtcNow();
        var lifetime = context.Options?.AccessTokenLifetime;
        var expiresAt = lifetime.HasValue && lifetime.Value > TimeSpan.Zero
            ? issuedAt + lifetime.Value
            : (DateTimeOffset?)null;

        var record = new AuthorityTokenDocument
        {
            TokenId = tokenId,
            Type = OpenIddictConstants.TokenTypeHints.AccessToken,
            SubjectId = document.ClientId,
            ClientId = document.ClientId,
            Scope = scopes.Count > 0 ? scopes.ToList() : new List<string>(),
            Status = "valid",
            CreatedAt = issuedAt,
            ExpiresAt = expiresAt
        };

        await tokenStore.InsertAsync(record, context.CancellationToken).ConfigureAwait(false);
        context.Transaction.Properties[AuthorityOpenIddictConstants.TokenTransactionProperty] = record;
    }
}

internal static class ClientCredentialHandlerHelpers
{
    public static IReadOnlyList<string> Split(IReadOnlyDictionary<string, string?> properties, string key)
    {
        if (!properties.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
        {
            return Array.Empty<string>();
        }

        return value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
    }

    public static (string[] Scopes, string? InvalidScope) ResolveGrantedScopes(
        IReadOnlyCollection<string> allowedScopes,
        IReadOnlyList<string> requestedScopes)
    {
        if (allowedScopes.Count == 0)
        {
            return (requestedScopes.Count == 0 ? Array.Empty<string>() : requestedScopes.ToArray(), null);
        }

        var allowed = new HashSet<string>(allowedScopes, StringComparer.Ordinal);

        if (requestedScopes.Count == 0)
        {
            return (allowedScopes.ToArray(), null);
        }

        foreach (var scope in requestedScopes)
        {
            if (!allowed.Contains(scope))
            {
                return (Array.Empty<string>(), scope);
            }
        }

        return (requestedScopes.ToArray(), null);
    }

    public static bool VerifySecret(string secret, string storedHash)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(secret);

        if (string.IsNullOrWhiteSpace(storedHash))
        {
            return false;
        }

        try
        {
            var computed = Convert.FromBase64String(AuthoritySecretHasher.ComputeHash(secret));
            var expected = Convert.FromBase64String(storedHash);

            if (computed.Length != expected.Length)
            {
                return false;
            }

            return CryptographicOperations.FixedTimeEquals(computed, expected);
        }
        catch (FormatException)
        {
            return false;
        }
    }
}
