using NSec.Cryptography;

namespace DodoSSH.Crypto;

/// <summary>
/// An Argon2id parameter set, in units that cannot be misread.
/// </summary>
/// <remarks>
/// <para>
/// This type exists for one reason: <c>NSec.Cryptography.Argon2Parameters.MemorySize</c> is in
/// <b>kibibytes</b>, not bytes. Passing bytes silently produces either a catastrophically weak
/// KDF or an impossible allocation — for a 256 MiB intent, <c>268435456</c> asks for 256 GiB,
/// while <c>262144</c> interpreted as bytes is 256 KiB and cracks in milliseconds.
/// </para>
/// <para>
/// Callers therefore never supply a raw memory figure. See docs/crypto.md §2.
/// </para>
/// <para>
/// Parallelism is pinned to 1 because libsodium's Argon2id supports only <c>p=1</c>. Memory
/// cost compensates: the default of 256 MiB at four passes is far above OWASP's 19 MiB/2-pass
/// floor, and measured about 323 ms on a fast desktop.
/// </para>
/// </remarks>
public sealed record Argon2Profile
{
    private const int KibibytesPerMebibyte = 1024;

    private Argon2Profile(int memoryMebibytes, int passes)
    {
        if (memoryMebibytes is < 8 or > 4096)
        {
            throw new ArgumentOutOfRangeException(
                nameof(memoryMebibytes),
                memoryMebibytes,
                "Memory must be between 8 and 4096 MiB.");
        }

        if (passes is < 1 or > 16)
        {
            throw new ArgumentOutOfRangeException(nameof(passes), passes, "Passes must be 1 to 16.");
        }

        MemoryMebibytes = memoryMebibytes;
        Passes = passes;
    }

    /// <summary>Memory cost, in mebibytes.</summary>
    public int MemoryMebibytes { get; }

    /// <summary>Number of passes over memory.</summary>
    public int Passes { get; }

    /// <summary>
    /// Degree of parallelism. Always 1; libsodium's Argon2id supports no other value, so this
    /// is a property of the algorithm rather than of a profile.
    /// </summary>
    public static int Parallelism => 1;

    /// <summary>Default for deriving the master key from a vault passphrase.</summary>
    public static Argon2Profile PassphraseDefault { get; } = new(256, 4);

    /// <summary>Reduced profile for low-powered devices. Still well above the OWASP floor.</summary>
    public static Argon2Profile PassphraseReduced { get; } = new(128, 3);

    /// <summary>Raised profile for users who accept a slower unlock.</summary>
    public static Argon2Profile PassphraseHigh { get; } = new(512, 4);

    /// <summary>
    /// Profile for 128-bit random secrets — recovery codes.
    /// </summary>
    /// <remarks>
    /// KDF hardening is nearly irrelevant for a full-entropy random secret; this is
    /// anti-nuisance only, not a defence against a serious offline attack.
    /// </remarks>
    public static Argon2Profile RandomSecret { get; } = new(64, 3);

    /// <summary>
    /// Reconstructs a profile from stored parameters.
    /// </summary>
    /// <remarks>
    /// KDF parameters are stored in plaintext per wrap row so that raising them later is a
    /// per-user, unlock-time migration rather than a breaking change, and so an older client
    /// can still open its own wrap.
    /// </remarks>
    /// <param name="memoryKibibytes">Stored memory cost, in kibibytes.</param>
    /// <param name="passes">Stored pass count.</param>
    /// <param name="parallelism">Stored parallelism. Must be 1.</param>
    public static Argon2Profile FromStoredParameters(int memoryKibibytes, int passes, int parallelism)
    {
        if (parallelism != 1)
        {
            throw new NotSupportedException(
                $"Argon2id parallelism {parallelism} is not supported; libsodium implements only p=1.");
        }

        if (memoryKibibytes % KibibytesPerMebibyte != 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(memoryKibibytes),
                memoryKibibytes,
                "Stored memory cost must be a whole number of mebibytes.");
        }

        return new Argon2Profile(memoryKibibytes / KibibytesPerMebibyte, passes);
    }

    /// <summary>Memory cost in kibibytes, as persisted and as NSec expects it.</summary>
    public int MemoryKibibytes => MemoryMebibytes * KibibytesPerMebibyte;

    /// <summary>Builds the NSec algorithm instance for this profile.</summary>
    public PasswordBasedKeyDerivationAlgorithm CreateAlgorithm() =>
        PasswordBasedKeyDerivationAlgorithm.Argon2id(new Argon2Parameters
        {
            // MemorySize is KiB. This single line is the reason this type exists.
            MemorySize = MemoryKibibytes,
            NumberOfPasses = Passes,
            DegreeOfParallelism = Parallelism,
        });
}
