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>Renames a vault.</summary>
/// <remarks>
/// <para>
/// Admin rather than Write, and the line is the one <c>UpdateTeamEndpoint</c> draws: a name is what
/// everybody in the vault sees it called, so changing it is an administrative act rather than an edit
/// to the vault's contents. A member who may add hosts to a shared vault may not rename it out from
/// under the people who share it.
/// </para>
/// <para>
/// Authenticated rather than Enrolled, unlike everything else about a vault here. A rename touches no
/// key material and needs none — somebody added to a team before they have finished setting their own
/// machine up can still be reading this screen — and requiring a published identity key would refuse
/// them for a reason that has nothing to do with what they are asking.
/// </para>
/// </remarks>
internal sealed class RenameVaultEndpoint(
    ICurrentUserContext currentUser,
    IVaultAccessService vaultAccess,
    VaultGrantService grants)
    : Endpoint<UpdateVaultRequest, Results<Ok<VaultSummary>, NotFound, ProblemHttpResult>>
{
    /// <inheritdoc />
    public override void Configure()
    {
        // PUT, for the reason the team rename is a PUT: one field, always sent whole, and a repeat is
        // the same vault rather than a second edit.
        Put("/api/v1/vaults/{vaultId:guid}");

        Policies(Auth.AuthenticatedPolicy);

        Description(b => b
            .WithName("RenameVault")
            .WithSummary("Renames a vault.")
            .WithTags("Vaults"));
    }

    /// <inheritdoc />
    public override async Task<Results<Ok<VaultSummary>, NotFound, ProblemHttpResult>> ExecuteAsync(
        UpdateVaultRequest 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.Admin))
        {
            return Problems.Coded(
                StatusCodes.Status403Forbidden,
                ProblemCodes.Forbidden,
                "Only an admin or the owner of the team that owns this vault can rename it.");
        }

        try
        {
            return TypedResults.Ok(
                await grants.RenameVaultAsync(access.Vault!, req, ct).ConfigureAwait(false));
        }
        catch (TeamInvalidException exception)
        {
            return Problems.Coded(
                StatusCodes.Status400BadRequest, ProblemCodes.InvalidTeam, exception.Message);
        }
    }
}

/// <summary>Deletes a vault.</summary>
/// <remarks>
/// <para>
/// Admin, like the rename above and for a stronger version of the same reason: this takes the vault away
/// from everybody in it at once, so it belongs to whoever administers the membership list rather than to
/// anybody who may write to its contents.
/// </para>
/// <para>
/// Authenticated rather than Enrolled, again like the rename. Deleting a vault touches no key material —
/// it withdraws grants rather than wrapping anything — so requiring a published identity key would refuse
/// an admin for a reason that has nothing to do with what they are asking.
/// </para>
/// <para>
/// <b>The team behind it is archived in a second call, and the two are deliberately not one
/// transaction.</b> Archiving is <c>TeamService</c>'s, it refuses while a team owns vaults, and it can
/// only tell that this one no longer does once the deletion is committed. A crash between the two leaves
/// an empty team — invisible on every screen, archivable afterwards by the endpoint that exists for it,
/// and harmless — which is a better failure than a vault that could not be deleted because tidying up its
/// team failed.
/// </para>
/// </remarks>
internal sealed class DeleteVaultEndpoint(
    ICurrentUserContext currentUser,
    IVaultAccessService vaultAccess,
    VaultGrantService grants,
    TeamService teams)
    : EndpointWithoutRequest<Results<NoContent, NotFound, ProblemHttpResult>>
{
    /// <inheritdoc />
    public override void Configure()
    {
        Delete("/api/v1/vaults/{vaultId:guid}");

        Policies(Auth.AuthenticatedPolicy);

        Description(b => b
            .WithName("DeleteVault")
            .WithSummary("Deletes a vault and withdraws every key to it.")
            .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))
        {
            // Indistinguishable from a vault that never existed, and from one somebody else deleted a
            // moment ago — which is what makes a repeat of this call safe to send.
            return TypedResults.NotFound();
        }

        if (!access.Permissions.HasFlag(PermissionFlags.Admin))
        {
            return Problems.Coded(
                StatusCodes.Status403Forbidden,
                ProblemCodes.Forbidden,
                "Only an admin or the owner of the team that owns this vault can delete it.");
        }

        try
        {
            var orphaned = await grants.DeleteVaultAsync(user, access.Vault!, ct).ConfigureAwait(false);

            if (orphaned is not null)
            {
                await teams.ArchiveAsync(user, orphaned, ct).ConfigureAwait(false);
            }

            return TypedResults.NoContent();
        }
        catch (VaultGrantInvalidException exception)
        {
            return Problems.Coded(
                StatusCodes.Status400BadRequest, ProblemCodes.InvalidVaultGrant, exception.Message);
        }
        catch (TeamNotEmptyException)
        {
            // The team gained a vault between the delete and the archive. The deletion stands and is what
            // was asked for; the team staying is not a failure the caller can or should act on.
            return TypedResults.NoContent();
        }
    }
}

/// <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>Moves this vault to a fresh key.</summary>
/// <remarks>
/// Gated on Share rather than on a rotation permission of its own. Rotating decides who can read what
/// is written next, which is the same question sharing and withdrawing answer, and a fourth permission
/// would be a distinction nobody administering a team would be able to explain.
/// </remarks>
internal sealed class RekeyVaultEndpoint(
    ICurrentUserContext currentUser,
    IVaultAccessService vaultAccess,
    VaultGrantService grants)
    : Endpoint<RekeyVaultRequest, Results<Ok<VaultSummary>, NotFound, ProblemHttpResult>>
{
    /// <inheritdoc />
    public override void Configure()
    {
        Post("/api/v1/vaults/{vaultId:guid}/rekey");

        Policies(Auth.EnrolledPolicy);

        Description(b => b
            .WithName("RekeyVault")
            .WithSummary("Advances this vault's key generation, wrapped to the caller.")
            .WithTags("Vaults"));
    }

    /// <inheritdoc />
    public override async Task<Results<Ok<VaultSummary>, NotFound, ProblemHttpResult>> ExecuteAsync(
        RekeyVaultRequest 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, so you cannot rotate its key.");
        }

        try
        {
            var summary = await grants
                .RekeyAsync(user, access.Vault!, (int)access.Permissions, req, ct)
                .ConfigureAwait(false);

            return TypedResults.Ok(summary);
        }
        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);
        }
    }
}
