using DodoSSH.Api.Authorization;
using DodoSSH.Api.Setup;
using DodoSSH.Contracts;
using DodoSSH.Domain.Authorization;
using FastEndpoints;
using Microsoft.AspNetCore.Http.HttpResults;

namespace DodoSSH.Api.Features.Teams;

/// <summary>Lists who can open a vault.</summary>
/// <remarks>
/// Read, not Share. Every member who can read a vault can already see the sharing graph — the server
/// stores it in plaintext and says so in docs/crypto.md §10 — so gating this on Share would hide from
/// the people it is about something the operator can read either way.
/// </remarks>
internal sealed class ListVaultGrantsEndpoint(
    ICurrentUserContext currentUser,
    IVaultAccessService vaultAccess,
    VaultGrantService grants)
    : EndpointWithoutRequest<Results<Ok<VaultGrantsResponse>, NotFound>>
{
    /// <inheritdoc />
    public override void Configure()
    {
        Get("/api/v1/vaults/{vaultId:guid}/grants");

        Policies(Auth.EnrolledPolicy);

        Description(b => b
            .WithName("ListVaultGrants")
            .WithSummary("Lists who holds a key to this vault.")
            .WithTags("Vaults"));
    }

    /// <inheritdoc />
    public override async Task<Results<Ok<VaultGrantsResponse>, NotFound>> ExecuteAsync(
        CancellationToken ct)
    {
        var user = await currentUser.GetOrProvisionAsync(ct).ConfigureAwait(false);
        var access = await vaultAccess
            .ResolveAsync(user.Id, Route<Guid>("vaultId"), ct)
            .ConfigureAwait(false);

        if (!access.Granted || !access.Permissions.HasFlag(PermissionFlags.Read))
        {
            return TypedResults.NotFound();
        }

        return TypedResults.Ok(await grants.ListGrantsAsync(access.Vault!, ct).ConfigureAwait(false));
    }
}

/// <summary>Wraps this vault's key to another member.</summary>
/// <remarks>
/// The one call in this API whose body the server can neither produce nor check. It stores a sealed
/// key and a signature over a tuple it never verifies — see docs/crypto.md §6 and §7 — which is
/// exactly why sharing is a client operation with a server-side record rather than a server feature.
/// </remarks>
internal sealed class IssueVaultGrantEndpoint(
    ICurrentUserContext currentUser,
    IVaultAccessService vaultAccess,
    VaultGrantService grants)
    : Endpoint<IssueVaultGrantRequest, Results<NoContent, NotFound, ProblemHttpResult>>
{
    /// <inheritdoc />
    public override void Configure()
    {
        Post("/api/v1/vaults/{vaultId:guid}/grants");

        Policies(Auth.EnrolledPolicy);

        Description(b => b
            .WithName("IssueVaultGrant")
            .WithSummary("Records a vault key wrapped to another member.")
            .WithTags("Vaults"));
    }

    /// <inheritdoc />
    public override async Task<Results<NoContent, NotFound, ProblemHttpResult>> ExecuteAsync(
        IssueVaultGrantRequest req,
        CancellationToken ct)
    {
        var user = await currentUser.GetOrProvisionAsync(ct).ConfigureAwait(false);
        var access = await vaultAccess
            .ResolveAsync(user.Id, Route<Guid>("vaultId"), ct)
            .ConfigureAwait(false);

        if (!access.Granted || !access.Permissions.HasFlag(PermissionFlags.Read))
        {
            return TypedResults.NotFound();
        }

        if (!access.Permissions.HasFlag(PermissionFlags.Share))
        {
            return Problems.Coded(
                StatusCodes.Status403Forbidden,
                ProblemCodes.Forbidden,
                "You do not have permission to share this vault.");
        }

        try
        {
            await grants.IssueGrantAsync(user, access.Vault!, req, ct).ConfigureAwait(false);

            // 204. There is nothing to return that the caller does not already hold — it produced
            // the wrap — and echoing the sealed key back would put it on the wire twice for nothing.
            return TypedResults.NoContent();
        }
        catch (VaultGrantInvalidException exception)
        {
            return Problems.Coded(
                StatusCodes.Status400BadRequest, ProblemCodes.InvalidVaultGrant, exception.Message);
        }
    }
}

/// <summary>Withdraws a member's key to this vault.</summary>
/// <remarks>
/// 404 for a member who holds no live grant, rather than a bland 204, for the reason device
/// revocation gives: "revoked" is what the user reads, and reading it about the wrong account is
/// worse than being told to look again. A caller driving towards "they cannot read this any more"
/// can treat 404 as having arrived.
/// </remarks>
internal sealed class RevokeVaultGrantEndpoint(
    ICurrentUserContext currentUser,
    IVaultAccessService vaultAccess,
    VaultGrantService grants)
    : EndpointWithoutRequest<Results<NoContent, NotFound, ProblemHttpResult>>
{
    /// <inheritdoc />
    public override void Configure()
    {
        Delete("/api/v1/vaults/{vaultId:guid}/grants/{userId:guid}");

        Policies(Auth.EnrolledPolicy);

        Description(b => b
            .WithName("RevokeVaultGrant")
            .WithSummary("Withdraws a member's key to this vault. Blocks future reads only.")
            .WithTags("Vaults"));
    }

    /// <inheritdoc />
    public override async Task<Results<NoContent, NotFound, ProblemHttpResult>> ExecuteAsync(
        CancellationToken ct)
    {
        var user = await currentUser.GetOrProvisionAsync(ct).ConfigureAwait(false);
        var access = await vaultAccess
            .ResolveAsync(user.Id, Route<Guid>("vaultId"), ct)
            .ConfigureAwait(false);

        if (!access.Granted || !access.Permissions.HasFlag(PermissionFlags.Read))
        {
            return TypedResults.NotFound();
        }

        if (!access.Permissions.HasFlag(PermissionFlags.Share))
        {
            return Problems.Coded(
                StatusCodes.Status403Forbidden,
                ProblemCodes.Forbidden,
                "You do not have permission to share this vault.");
        }

        try
        {
            var revoked = await grants
                .RevokeGrantAsync(user, access.Vault!, Route<Guid>("userId"), ct)
                .ConfigureAwait(false);

            return revoked ? TypedResults.NoContent() : TypedResults.NotFound();
        }
        catch (VaultGrantInvalidException exception)
        {
            return Problems.Coded(
                StatusCodes.Status400BadRequest, ProblemCodes.InvalidVaultGrant, exception.Message);
        }
    }
}
