using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Security.Cryptography;

namespace DodoSSH.Crypto;

/// <summary>
/// Identifies the row a ciphertext belongs to, and computes the additional authenticated data
/// that binds the ciphertext to it.
/// </summary>
/// <remarks>
/// <para>
/// See docs/crypto.md §4, which is normative. The AAD is never stored: it is recomputed from
/// the row's plaintext columns on both encrypt and decrypt. That is what stops a server that
/// holds every ciphertext from moving one between rows, rolling a row back to an earlier key
/// generation, replaying a revoked grant, or substituting a metadata blob for a payload blob.
/// None of those properties follow from access control.
/// </para>
/// <para>
/// The encoding is fixed-width rather than delimited, so no field value can forge a field
/// boundary.
/// </para>
/// </remarks>
/// <param name="Purpose">What this ciphertext is.</param>
/// <param name="ResourceType">The kind of entity it belongs to.</param>
/// <param name="ResourceId">The entity's identifier.</param>
/// <param name="KeyId">The key it is encrypted under, where one is identified.</param>
/// <param name="KeyGeneration">The vault key generation in force.</param>
/// <param name="ItemVersion">The item version, where applicable.</param>
/// <param name="AadVersion">The AAD rule version, so a change can be applied lazily.</param>
/// <param name="SchemaVersion">The relational schema version.</param>
[StructLayout(LayoutKind.Auto)]
public readonly record struct AadDescriptor(
    CryptoSpec.AadPurpose Purpose,
    CryptoSpec.AadResourceType ResourceType,
    Guid ResourceId,
    Guid KeyId,
    uint KeyGeneration,
    uint ItemVersion,
    byte AadVersion,
    ushort SchemaVersion)
{
    private const int OffsetAadVersion = 5;
    private const int OffsetPurpose = 6;
    private const int OffsetResourceType = 7;
    private const int OffsetResourceId = 8;
    private const int OffsetKeyId = 24;
    private const int OffsetKeyGeneration = 40;
    private const int OffsetItemVersion = 44;
    private const int OffsetSchemaVersion = 48;

    /// <summary>
    /// Creates a descriptor at the current AAD and schema versions.
    /// </summary>
    public static AadDescriptor Create(
        CryptoSpec.AadPurpose purpose,
        CryptoSpec.AadResourceType resourceType,
        Guid resourceId,
        Guid keyId = default,
        uint keyGeneration = 1,
        uint itemVersion = 0) =>
        new(
            purpose,
            resourceType,
            resourceId,
            keyId,
            keyGeneration,
            itemVersion,
            CryptoSpec.CurrentAadVersion,
            CryptoSpec.CurrentSchemaVersion);

    /// <summary>
    /// Writes the canonical 64-byte encoding.
    /// </summary>
    /// <remarks>
    /// UUIDs are written in RFC 4122 big-endian order, <b>not</b> the mixed-endian order that
    /// <see cref="Guid.ToByteArray()"/> produces by default. Getting that wrong would make
    /// ciphertext written by one implementation undecryptable by another.
    /// </remarks>
    public void WriteCanonicalEncoding(Span<byte> destination)
    {
        if (destination.Length < CryptoSpec.AadEncodedLength)
        {
            throw new ArgumentException(
                $"Destination must be at least {CryptoSpec.AadEncodedLength} bytes.",
                nameof(destination));
        }

        if (Purpose == CryptoSpec.AadPurpose.Unspecified)
        {
            throw new InvalidOperationException("AAD purpose must be specified.");
        }

        var buffer = destination[..CryptoSpec.AadEncodedLength];
        buffer.Clear();

        CryptoSpec.AadMagic.CopyTo(buffer);
        buffer[OffsetAadVersion] = AadVersion;
        buffer[OffsetPurpose] = (byte)Purpose;
        buffer[OffsetResourceType] = (byte)ResourceType;

        if (!ResourceId.TryWriteBytes(buffer[OffsetResourceId..], bigEndian: true, out _))
        {
            throw new InvalidOperationException("Failed to write resource id.");
        }

        if (!KeyId.TryWriteBytes(buffer[OffsetKeyId..], bigEndian: true, out _))
        {
            throw new InvalidOperationException("Failed to write key id.");
        }

        BinaryPrimitives.WriteUInt32BigEndian(buffer[OffsetKeyGeneration..], KeyGeneration);
        BinaryPrimitives.WriteUInt32BigEndian(buffer[OffsetItemVersion..], ItemVersion);
        BinaryPrimitives.WriteUInt16BigEndian(buffer[OffsetSchemaVersion..], SchemaVersion);

        // Trailing 14 reserved bytes stay zero from the Clear above.
    }

    /// <summary>Returns the canonical encoding as a new array. Prefer the span overload.</summary>
    public byte[] ToCanonicalEncoding()
    {
        var buffer = new byte[CryptoSpec.AadEncodedLength];
        WriteCanonicalEncoding(buffer);
        return buffer;
    }

    /// <summary>Computes the AAD: SHA-256 over the canonical encoding.</summary>
    public void ComputeAad(Span<byte> destination)
    {
        Span<byte> encoded = stackalloc byte[CryptoSpec.AadEncodedLength];
        WriteCanonicalEncoding(encoded);

        if (!SHA256.TryHashData(encoded, destination, out _))
        {
            throw new ArgumentException(
                $"Destination must be at least {CryptoSpec.DigestSize} bytes.",
                nameof(destination));
        }
    }

    /// <summary>Computes the AAD as a new array.</summary>
    public byte[] ComputeAad()
    {
        var aad = new byte[CryptoSpec.DigestSize];
        ComputeAad(aad);
        return aad;
    }
}
