using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;

// The handler pushes the gateway's constraint-bearing identity; alias away the shared library's
// ApiKeyIdentity so the unqualified name resolves to the gateway type.
using ApiKeyIdentity = ZB.MOM.WW.MxGateway.Server.Security.Authentication.ApiKeyIdentity;

namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization;

public sealed class GatewayGrpcAuthorizationInterceptor(
    IApiKeyVerifier apiKeyVerifier,
    GatewayGrpcScopeResolver scopeResolver,
    IGatewayRequestIdentityAccessor identityAccessor,
    IOptions<GatewayOptions> options) : Interceptor
{
    /// <inheritdoc />
    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        ApiKeyIdentity? identity = await AuthenticateAndAuthorizeAsync(request, context).ConfigureAwait(false);
        IDisposable? identityScope = identity is null ? null : identityAccessor.Push(identity);
        using (identityScope)
        {
            return await continuation(request, context).ConfigureAwait(false);
        }
    }

    /// <inheritdoc />
    public override async Task ServerStreamingServerHandler<TRequest, TResponse>(
        TRequest request,
        IServerStreamWriter<TResponse> responseStream,
        ServerCallContext context,
        ServerStreamingServerMethod<TRequest, TResponse> continuation)
    {
        ApiKeyIdentity? identity = await AuthenticateAndAuthorizeAsync(request, context).ConfigureAwait(false);
        IDisposable? identityScope = identity is null ? null : identityAccessor.Push(identity);
        using (identityScope)
        {
            await continuation(request, responseStream, context).ConfigureAwait(false);
        }
    }

    /// <summary>Authenticates the API key and authorizes the RPC call by required scope.</summary>
    /// <typeparam name="TRequest">Request message type.</typeparam>
    /// <param name="request">Request payload.</param>
    /// <param name="context">RPC server call context.</param>
    /// <returns>Authenticated API key identity, or null if authentication is disabled.</returns>
    private async Task<ApiKeyIdentity?> AuthenticateAndAuthorizeAsync<TRequest>(
        TRequest request,
        ServerCallContext context)
        where TRequest : class
    {
        if (options.Value.Authentication.Mode == AuthenticationMode.Disabled)
        {
            return null;
        }

        string? authorizationHeader = context.RequestHeaders.GetValue("authorization");

        // The shared verifier owns parse + pepper + lookup + revocation + constant-time compare,
        // returning a discriminated failure rather than throwing. Every authentication failure maps
        // to Unauthenticated with an opaque message; the client never learns which stage failed.
        ApiKeyVerification verification = await apiKeyVerifier
            .VerifyAsync(authorizationHeader ?? string.Empty, context.CancellationToken)
            .ConfigureAwait(false);

        if (!verification.Succeeded || verification.Identity is null)
        {
            throw new RpcException(new Status(
                StatusCode.Unauthenticated,
                "Missing or invalid API key."));
        }

        ApiKeyIdentity identity = GatewayApiKeyIdentityMapper.ToGatewayIdentity(verification.Identity);

        string requiredScope = scopeResolver.ResolveRequiredScope(request);
        if (!identity.Scopes.Contains(requiredScope))
        {
            throw new RpcException(new Status(
                StatusCode.PermissionDenied,
                $"API key is missing required scope '{requiredScope}'."));
        }

        return identity;
    }
}
