using System.Security.Cryptography;
using DodoSSH.Client.Domain;
using DodoSSH.Contracts;
using DodoSSH.Crypto;

namespace DodoSSH.Client.Sync;

/// <summary>
/// Turns a known host key into an item payload and back.
/// </summary>
/// <remarks>
/// <para>
/// Mirrors <see cref="HostCipher"/> exactly, including the rule that a payload is sealed at the version the
/// server <em>will</em> assign rather than the one it replaces — see <see cref="SyncVersions"/>.
/// </para>
/// <para>
/// <b>The resource type is the one thing not to copy.</b> As with <see cref="CredentialCipher"/>, the AAD
/// binds it and the two enums that name item types do not agree: <c>SyncEntityType.KnownHostKey</c> is 10
/// while <c>CryptoSpec.AadResourceType.KnownHostKey</c> is 11, because the crypto enum carries None, User,
/// Device and Vault ahead of the item types. Casting one to the other would seal a pin under the resource
/// type for a <em>port forward</em> — which encrypts perfectly, decrypts perfectly on the machine that wrote
/// it, and is a specification violation nothing would notice until an interoperating client refused the item.
/// </para>
/// </remarks>
public static class KnownHostKeyCipher
{
    private const CryptoSpec.AadResourceType Resource = CryptoSpec.AadResourceType.KnownHostKey;

    /// <summary>Encrypts a pin.</summary>
    /// <param name="knownHost">The pin. Must be valid for storage.</param>
    /// <param name="vaultKey">The vault key, which the data key is wrapped under.</param>
    /// <param name="entityId">The item id, which the AAD binds.</param>
    /// <param name="keyGeneration">The vault's current key generation.</param>
    /// <param name="itemVersion">The version this payload will hold once the server accepts it.</param>
    public static EncryptedPayload Seal(
        KnownHostSecret knownHost,
        ReadOnlySpan<byte> vaultKey,
        Guid entityId,
        uint keyGeneration,
        int itemVersion)
    {
        ArgumentNullException.ThrowIfNull(knownHost);
        ArgumentOutOfRangeException.ThrowIfLessThan(itemVersion, 1);

        var plaintext = KnownHostSecretCodec.Encode(knownHost);
        var dataKey = ItemKeys.CreateDataKey();

        try
        {
            var dataKeyId = Guid.CreateVersion7();

            var wrappedDataKey = ItemKeys.WrapDataKey(
                dataKey, vaultKey, Resource, entityId, keyGeneration, (uint)itemVersion);

            var envelope = ItemKeys.SealPayload(
                dataKey, plaintext, Resource, entityId, dataKeyId, keyGeneration, (uint)itemVersion);

            return new EncryptedPayload(
                envelope, wrappedDataKey, dataKeyId, keyGeneration, CryptoSpec.CurrentAadVersion);
        }
        finally
        {
            CryptographicOperations.ZeroMemory(dataKey);

            // Wiped like every other payload in this folder, and for a smaller reason than the others: a
            // fingerprint is published on purpose, so nothing in here is a secret in the sense a password is.
            // What the buffer does hold is the address of a machine this user reaches, and leaving that in a
            // pooled buffer for nothing would be a gratuitous difference from the neighbouring ciphers.
            CryptographicOperations.ZeroMemory(plaintext);
        }
    }

    /// <summary>Decrypts a pin.</summary>
    /// <inheritdoc cref="HostCipher.TryOpen" path="/returns" />
    public static KnownHostSecretDocument? TryOpen(
        EncryptedPayload payload,
        ReadOnlySpan<byte> vaultKey,
        Guid entityId,
        int itemVersion)
    {
        ArgumentNullException.ThrowIfNull(payload);

        if (itemVersion < 1 || payload.WrappedDataKey.Length == 0)
        {
            return null;
        }

        var dataKey = ItemKeys.TryUnwrapDataKey(
            vaultKey,
            payload.WrappedDataKey,
            Resource,
            entityId,
            payload.KeyGeneration,
            (uint)itemVersion);

        if (dataKey is null)
        {
            return null;
        }

        try
        {
            var plaintext = ItemKeys.TryOpenPayload(
                dataKey,
                payload.Envelope,
                Resource,
                entityId,
                payload.DataKeyId,
                payload.KeyGeneration,
                (uint)itemVersion);

            if (plaintext is null)
            {
                return null;
            }

            try
            {
                return KnownHostSecretCodec.TryDecode(plaintext, out var document) ? document : null;
            }
            finally
            {
                CryptographicOperations.ZeroMemory(plaintext);
            }
        }
        finally
        {
            CryptographicOperations.ZeroMemory(dataKey);
        }
    }
}
