using System.Security.Cryptography;
using DodoSSH.Client.Api;
using DodoSSH.Client.Storage;
using DodoSSH.Client.Sync;
using DodoSSH.Contracts;
using DodoSSH.Crypto;

namespace DodoSSH.Client.Session;

/// <summary>What a share attempt did.</summary>
/// <param name="Shared">Whether a grant was recorded.</param>
/// <param name="Verification">
/// How the recipient's key was checked. Present whether or not the share went ahead, because a refusal
/// is the interesting outcome and the reason for it is the whole of what a user needs to see.
/// </param>
/// <param name="Message">One line for a person. Never contains key material.</param>
public sealed record ShareOutcome(
    bool Shared,
    RecipientVerification Verification,
    string Message);

/// <summary>
/// Sharing, from the side that holds the keys.
/// </summary>
/// <remarks>
/// <para>
/// These live on <see cref="VaultSession"/> rather than in a service above it for the reason
/// registering a device does: wrapping a vault key is the one step only an unlocked session can
/// perform, and this type is the keyring's custodian. Everything else — the calls, the directory —
/// arrives as a parameter, so the session still knows nothing about how either is implemented.
/// </para>
/// <para>
/// <b>Nothing here trusts the server's answer about somebody else's key.</b> Every share reads the
/// whole key log, verifies its hash chain, and refuses unless the directory's answer appears in it
/// unchanged. That check is the difference between end-to-end encryption and a server that can read
/// everything by handing out a key of its own; see <see cref="KeyLogAudit"/> and ADR 0001.
/// </para>
/// </remarks>
public sealed partial class VaultSession
{
    /// <summary>
    /// Creates a vault owned by a team, generating its key here.
    /// </summary>
    /// <param name="api">The team calls.</param>
    /// <param name="teamId">The owning team.</param>
    /// <param name="name">Display name. Plaintext, as all vault names are.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The new vault, already readable by this session.</returns>
    /// <remarks>
    /// The key never leaves this process in the clear: it is generated here, sealed to this user's own
    /// encryption key, and the seal is what the server stores. The creator's grant carries no key log
    /// head, exactly as a personal vault's does not — there is no third party whose key could have been
    /// substituted when you wrap something to yourself.
    /// </remarks>
    public async Task<StoredVault> CreateTeamVaultAsync(
        ITeamApi api,
        Guid teamId,
        string name,
        CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(disposed, this);
        ArgumentNullException.ThrowIfNull(api);
        ArgumentException.ThrowIfNullOrWhiteSpace(name);

        var vaultId = Guid.CreateVersion7();
        var vaultKey = VaultKeys.Create();
        var now = clock.GetUtcNow();

        try
        {
            var request = BuildCreateRequest(vaultId, vaultKey, name, now);

            var summary = await api.CreateTeamVaultAsync(teamId, request, cancellationToken)
                .ConfigureAwait(false);

            var stored = ToStored(summary);

            await Vault.UpsertAsync(stored, cancellationToken).ConfigureAwait(false);

            // Adopted rather than unwrapped from the response: this process generated the key, so
            // unwrapping the server's copy of our own seal would be a round trip to learn something we
            // already know. The keyring takes ownership from here.
            keyring.Adopt(vaultId, vaultKey, summary.KeyGeneration);

            Vaults = await Vault.ListAsync(cancellationToken).ConfigureAwait(false);

            return stored;
        }
        catch
        {
            // Never reached the keyring, so this is the only thing that can release it.
            CryptographicOperations.ZeroMemory(vaultKey);
            throw;
        }
    }

    /// <summary>
    /// Wraps a vault's key to another member, after verifying their published key.
    /// </summary>
    /// <param name="grants">The grant calls.</param>
    /// <param name="directory">The directory and the key log that makes it checkable.</param>
    /// <param name="vaultId">The vault to share.</param>
    /// <param name="recipientUserId">Who to share it with.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <remarks>
    /// <para>
    /// The verification is not optional and is not a parameter. A caller that could pass
    /// <c>skipChecks: true</c> is a caller that will, on the day the log is briefly unreachable, and the
    /// resulting grant is indistinguishable from a correct one afterwards.
    /// </para>
    /// <para>
    /// What this still cannot promise is that the key belongs to the person you meant. Compare
    /// <see cref="VerifiedRecipient.Fingerprint"/> with them over a channel this server does not carry;
    /// that is the only step that closes the gap, and the outcome message says so.
    /// </para>
    /// </remarks>
    public async Task<ShareOutcome> ShareVaultAsync(
        IVaultGrantApi grants,
        IDirectoryApi directory,
        Guid vaultId,
        Guid recipientUserId,
        CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(disposed, this);
        ArgumentNullException.ThrowIfNull(grants);
        ArgumentNullException.ThrowIfNull(directory);

        if (!keyring.TryGet(vaultId, out var vaultKey, out var keyGeneration))
        {
            throw new VaultUnreadableException(vaultId);
        }

        var entry = await directory.LookupByIdAsync(recipientUserId, cancellationToken)
            .ConfigureAwait(false);

        var log = await KeyLogAudit.ReadAsync(directory, cancellationToken).ConfigureAwait(false);
        var verification = KeyLogAudit.Verify(log, entry);

        if (!verification.IsVerified)
        {
            return new ShareOutcome(false, verification, verification.Message);
        }

        var recipient = verification.Recipient!;

        await IssueAsync(grants, vaultId, vaultKey, keyGeneration, recipient, cancellationToken)
            .ConfigureAwait(false);

        return new ShareOutcome(
            true,
            verification,
            "Shared. Check the fingerprint with them out of band — everything the client can verify on "
            + "its own only proves this server has been consistent with itself.");
    }

    /// <summary>
    /// Re-reads which vaults the server says are reachable, and opens any that have become readable.
    /// </summary>
    /// <returns>How many vaults this call made readable that were not before.</returns>
    /// <remarks>
    /// Called after a share and on a periodic pass. A vault somebody shared a minute ago arrives as a
    /// new entry with a wrapped key attached; one whose grant was revoked arrives without one, and is
    /// marked unreadable rather than quietly dropped so the interface can say what happened. Items
    /// already pulled are deliberately left alone — see <see cref="VaultStore.ReplaceAllAsync"/>.
    /// </remarks>
    public async Task<int> RefreshVaultsAsync(IAccountApi api, CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(disposed, this);
        ArgumentNullException.ThrowIfNull(api);

        var me = await api.GetMeAsync(cancellationToken).ConfigureAwait(false);

        await Vault.ReplaceAllAsync([.. me.Vaults.Select(ToStored)], cancellationToken)
            .ConfigureAwait(false);

        Vaults = await Vault.ListAsync(cancellationToken).ConfigureAwait(false);

        var admitted = 0;

        foreach (var vault in Vaults)
        {
            if (keyring.CanRead(vault.VaultId))
            {
                continue;
            }

            if (keyring.TryAdmit(bundle, vault))
            {
                admitted++;
            }
            else
            {
                keyring.MarkUnreadable(vault.VaultId);
            }
        }

        return admitted;
    }

    /// <summary>Signs and posts one grant.</summary>
    private async Task IssueAsync(
        IVaultGrantApi grants,
        Guid vaultId,
        ReadOnlyMemory<byte> vaultKey,
        uint keyGeneration,
        VerifiedRecipient recipient,
        CancellationToken cancellationToken)
    {
        var now = clock.GetUtcNow();
        var entry = recipient.Entry;

        var wrapped = VaultKeys.WrapTo(
            vaultKey.Span, entry.EncryptionPublicKey, vaultId, keyGeneration);

        var ownFingerprint = DshCrypto.ComputeFingerprint(
            bundle.EncryptionPublicKey, bundle.SigningPublicKey);

        var canonical = GrantStatementCodec.Encode(
            vaultId,
            keyGeneration,
            GrantPurpose.Member,
            granteeUserId: entry.UserId,
            granteeKeyFingerprint: recipient.Fingerprint,
            wrappedKey: wrapped,
            granterUserId: Profile.UserId,
            granterKeyFingerprint: ownFingerprint,

            // Present, unlike a self-grant's. This is the third-party case the head exists for: it
            // records which view of the key log this client held while wrapping, so a server showing
            // two clients different logs has to keep both stories straight for ever after.
            keyLogHead: recipient.KeyLogHead,
            grantedAt: now);

        await grants.IssueVaultGrantAsync(
                vaultId,
                new IssueVaultGrantRequest(
                    RecipientUserId: entry.UserId,
                    RecipientKeyFingerprint: recipient.Fingerprint,
                    KeyGeneration: keyGeneration,
                    WrappedVaultKey: wrapped,
                    KeyLogHead: recipient.KeyLogHead,
                    GrantSignature: GrantStatementCodec.Sign(bundle.SigningKey, canonical),
                    GrantedAt: now),
                cancellationToken)
            .ConfigureAwait(false);
    }

    /// <remarks>
    /// The signature covers the vault id, so the id has to be chosen before anything is wrapped — which
    /// is also what makes a create whose response was lost safe to send again.
    /// </remarks>
    private CreateTeamVaultRequest BuildCreateRequest(
        Guid vaultId,
        byte[] vaultKey,
        string name,
        DateTimeOffset now)
    {
        var wrapped = VaultKeys.WrapTo(vaultKey, bundle.EncryptionPublicKey, vaultId, 1);

        var fingerprint = DshCrypto.ComputeFingerprint(
            bundle.EncryptionPublicKey, bundle.SigningPublicKey);

        var canonical = GrantStatementCodec.Encode(
            vaultId,
            keyGeneration: 1,
            GrantPurpose.Member,
            granteeUserId: Profile.UserId,
            granteeKeyFingerprint: fingerprint,
            wrappedKey: wrapped,
            granterUserId: Profile.UserId,
            granterKeyFingerprint: fingerprint,
            keyLogHead: default,
            grantedAt: now);

        return new CreateTeamVaultRequest(
            VaultId: vaultId,
            Name: name,
            WrappedVaultKey: wrapped,
            GrantSignature: GrantStatementCodec.Sign(bundle.SigningKey, canonical),
            GrantedAt: now);
    }

    private static StoredVault ToStored(VaultSummary summary) =>
        new(
            summary.VaultId,
            summary.Name,
            summary.IsPersonal,
            summary.TeamId,
            summary.KeyGeneration,
            summary.Permissions,
            summary.WrappedVaultKey,
            summary.RekeyRequired);
}
