using DodoSSH.Contracts;
using DodoSSH.Infrastructure;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.EntityFrameworkCore;

namespace DodoSSH.Api.Authorization;

/// <summary>
/// Requires that the caller has published an identity key.
/// </summary>
/// <remarks>
/// Not a confidentiality boundary — vault access is decided by ownership and grants, and an
/// unenrolled user holds neither. What this prevents is a client with no key material writing
/// ciphertext nobody can ever decrypt, and reading vault contents it has no way to open. Both
/// present to a user as data corruption, so the server refuses early and says why.
/// </remarks>
internal sealed class EnrolledRequirement : IAuthorizationRequirement;

/// <summary>Checks enrollment state against the database.</summary>
/// <remarks>
/// Scoped, not singleton, because it uses the request's <see cref="DodoDbContext"/>. Registering it
/// with the default singleton lifetime would capture one context for the process.
/// </remarks>
internal sealed class EnrolledHandler(
    ICurrentUserContext currentUser,
    DodoDbContext database,
    IHttpContextAccessor accessor)
    : AuthorizationHandler<EnrolledRequirement>
{
    /// <inheritdoc />
    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        EnrolledRequirement requirement)
    {
        ArgumentNullException.ThrowIfNull(context);

        // Anonymous callers are already refused by the policy's authentication requirement, which
        // produces a 401. Leaving this one unmet as well would turn that into a 403.
        if (context.User.Identity?.IsAuthenticated != true)
        {
            return;
        }

        var cancellationToken = accessor.HttpContext?.RequestAborted ?? CancellationToken.None;

        var user = await currentUser.GetOrProvisionAsync(cancellationToken).ConfigureAwait(false);

        var enrolled = await database.UserKeys
            .AnyAsync(k => k.UserId == user.Id && k.IsCurrent, cancellationToken)
            .ConfigureAwait(false);

        if (enrolled)
        {
            context.Succeed(requirement);
        }
    }
}

/// <summary>
/// Turns an unmet <see cref="EnrolledRequirement"/> into a ProblemDetails response carrying
/// <see cref="ProblemCodes.EnrollmentRequired"/>.
/// </summary>
/// <remarks>
/// A bare 403 has an empty body, so a client cannot tell "you have not enrolled" — which it can
/// fix on its own — from "you may not touch this vault", which it cannot. Every other failure falls
/// through to the framework's default handling unchanged.
/// </remarks>
internal sealed class DodoAuthorizationResultHandler : IAuthorizationMiddlewareResultHandler
{
    private readonly AuthorizationMiddlewareResultHandler defaultHandler = new();

    /// <inheritdoc />
    public async Task HandleAsync(
        RequestDelegate next,
        HttpContext context,
        AuthorizationPolicy policy,
        PolicyAuthorizationResult authorizeResult)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(authorizeResult);

        var enrollmentMissing = authorizeResult.Forbidden
            && authorizeResult.AuthorizationFailure is { } failure
            && failure.FailedRequirements.OfType<EnrolledRequirement>().Any();

        if (!enrollmentMissing)
        {
            await defaultHandler.HandleAsync(next, context, policy, authorizeResult).ConfigureAwait(false);
            return;
        }

        await TypedResults.Problem(
                detail: "Publish an identity key at POST /api/v1/me/enrollment before using vaults.",
                statusCode: StatusCodes.Status403Forbidden,
                type: ProblemCodes.TypeBaseUri + ProblemCodes.EnrollmentRequired,
                extensions: new Dictionary<string, object?>(StringComparer.Ordinal)
                {
                    ["code"] = ProblemCodes.EnrollmentRequired,
                })
            .ExecuteAsync(context)
            .ConfigureAwait(false);
    }
}
