using System.Security.Cryptography;
using NSec.Cryptography;

namespace DodoSSH.Client.Ssh;

/// <summary>Which kind of key pair to make.</summary>
public enum SshKeyAlgorithm
{
    /// <summary>Ed25519. Small, fast, and what every current OpenSSH prefers.</summary>
    Ed25519 = 0,

    /// <summary>RSA at 4096 bits, for servers too old to accept the above.</summary>
    Rsa4096 = 1,
}

/// <summary>
/// A freshly generated key pair, in the two forms anybody needs it in.
/// </summary>
/// <param name="PrivateKeyArmour">
/// The private half, in the armoured form <c>ssh-keygen</c> writes. Goes straight into
/// <c>SshKeySecret.PrivateKeyPem</c>, which stores it verbatim.
/// </param>
/// <param name="PublicKeyLine">
/// The public half, as one <c>authorized_keys</c> line. This is what gets installed on a host.
/// </param>
/// <param name="Fingerprint">
/// The <c>SHA256:…</c> fingerprint, in the format <c>ssh-keygen -lf</c> prints, so it can be read out to
/// somebody or compared against what a host reports.
/// </param>
public sealed record GeneratedSshKey(string PrivateKeyArmour, string PublicKeyLine, string Fingerprint);

/// <summary>
/// Makes a new SSH key pair without shelling out to <c>ssh-keygen</c>.
/// </summary>
/// <remarks>
/// <para>
/// <b>Why the client can do this at all.</b> Every part is already here: NSec does Ed25519 because .NET
/// does not, the BCL does RSA, and the SSH wire encoding is a few length-prefixed strings — see
/// <see cref="OpenSshKeyWriter"/>. What it buys is that the private key is never written to a disk. The
/// alternative flow is "run ssh-keygen, find the file, open it, copy the text, paste it here, remember to
/// delete the file", and the last step is the one nobody does.
/// </para>
/// <para>
/// <b>The armour has no passphrase</b>, and that is a deliberate limitation with its reasoning in
/// <see cref="OpenSshKeyWriter"/>. The key is protected by the keychain it lands in.
/// </para>
/// <para>
/// This lives in the SSH project rather than in <c>DodoSSH.Crypto</c>, which is the normative
/// implementation of <c>docs/crypto.md</c> and has nothing to say about SSH file formats. It is also where
/// <see cref="SshHostKeyFingerprint"/> already lives, and a second <c>SHA256:</c> encoder would be a second
/// thing to get wrong.
/// </para>
/// </remarks>
public static class SshKeyGenerator
{
    /// <summary>
    /// Generates a key pair.
    /// </summary>
    /// <param name="algorithm">Which kind.</param>
    /// <param name="comment">
    /// The trailing comment, conventionally <c>user@machine</c>. It identifies the key in a host's
    /// <c>authorized_keys</c> and is the only thing there that will say where it came from.
    /// </param>
    /// <remarks>
    /// Synchronous and CPU-bound. RSA at 4096 bits is seconds of work on an ordinary machine, so a caller on
    /// a UI thread has to move this to one of its own — the window would otherwise freeze at exactly the
    /// moment somebody is watching it. Ed25519 is effectively instant, and the caller should not have to
    /// know which is which.
    /// </remarks>
    public static GeneratedSshKey Generate(SshKeyAlgorithm algorithm, string comment) => algorithm switch
    {
        SshKeyAlgorithm.Ed25519 => Ed25519(comment),
        SshKeyAlgorithm.Rsa4096 => Rsa4096(comment),
        _ => throw new ArgumentOutOfRangeException(nameof(algorithm)),
    };

    private static GeneratedSshKey Ed25519(string comment)
    {
        var parameters = new KeyCreationParameters
        {
            // The seed has to come back out to be written into the file. NSec holds key material in
            // libsodium's guarded memory and refuses to export it unless asked at creation time.
            ExportPolicy = KeyExportPolicies.AllowPlaintextExport,
        };

        using var key = Key.Create(SignatureAlgorithm.Ed25519, parameters);

        var seed = key.Export(KeyBlobFormat.RawPrivateKey);

        try
        {
            var publicKey = key.PublicKey.Export(KeyBlobFormat.RawPublicKey);

            return new GeneratedSshKey(
                OpenSshKeyWriter.WriteEd25519PrivateKey(seed, publicKey, comment),
                OpenSshKeyWriter.WriteEd25519PublicKey(publicKey, comment),
                SshHostKeyFingerprint.Format(OpenSshKeyWriter.Ed25519PublicBlob(publicKey)));
        }
        finally
        {
            // The one copy of the private scalar this method makes, and it is an ordinary managed array
            // outside libsodium's guarded memory. Clearing it does not undo anything the garbage collector
            // may already have moved, which is why the export happens once and is used immediately.
            CryptographicOperations.ZeroMemory(seed);
        }
    }

    /// <remarks>
    /// PKCS#1, which is what <c>ExportRSAPrivateKeyPem</c> writes and what SSH.NET's <c>RSA PRIVATE KEY</c>
    /// branch reads. No hand-encoding is needed on this path at all — only the public line, because there is
    /// no BCL helper for the SSH wire format.
    /// </remarks>
    private static GeneratedSshKey Rsa4096(string comment)
    {
        using var rsa = RSA.Create(4096);

        return new GeneratedSshKey(
            rsa.ExportRSAPrivateKeyPem() + "\n",
            OpenSshKeyWriter.WriteRsaPublicKey(rsa, comment),
            SshHostKeyFingerprint.Format(OpenSshKeyWriter.RsaPublicBlob(rsa)));
    }
}
