using System.Security.Cryptography;
using System.Text;

namespace ClawdDotNet.Core.Security;

/// <summary>
/// Verschlüsselt Werte mit einer Passphrase statt mit DPAPI.
///
/// Hintergrund: <see cref="SecretProtector"/> nutzt DPAPI im Benutzerkontext —
/// entschlüsseln kann nur derselbe Windows-Benutzer auf demselben Rechner. Für den
/// laufenden Betrieb ist das richtig, für ein Backup jedoch untauglich: Ein Backup
/// wird gerade dann gebraucht, wenn der Rechner defekt ist. Die Zugangsdaten darin
/// wären auf dem Ersatzrechner nicht lesbar.
///
/// Deshalb werden Zugangsdaten beim Sichern auf eine Passphrase umgeschlüsselt und
/// beim Wiederherstellen zurück auf DPAPI.
///
/// Aufbau eines geschützten Werts:
/// <code>pbe:v1:&lt;salt&gt;:&lt;nonce&gt;:&lt;tag&gt;:&lt;ciphertext&gt;</code>
/// Alle Teile Base64. Jeder Wert bekommt ein eigenes Salz und einen eigenen Nonce —
/// gleiche Klartexte ergeben dadurch unterschiedliche Chiffrate.
/// </summary>
public static class PassphraseProtector
{
    private const string Prefix = "pbe:v1:";

    private const int SaltBytes = 16;
    private const int NonceBytes = 12;   // AES-GCM Standard
    private const int TagBytes = 16;
    private const int KeyBytes = 32;     // AES-256

    /// <summary>
    /// Rundenzahl der Schlüsselableitung. Hoch genug, um Rateversuche teuer zu machen,
    /// niedrig genug für ein Backup mit vielen Einzelwerten.
    /// </summary>
    private const int Iterations = 210_000;

    public static bool IsProtected(string? value)
        => value?.StartsWith(Prefix, StringComparison.Ordinal) == true;

    public static string? Protect(string? plainText, string passphrase)
    {
        if (string.IsNullOrEmpty(plainText))
            return plainText;

        if (string.IsNullOrEmpty(passphrase))
            throw new ArgumentException("Passphrase darf nicht leer sein.", nameof(passphrase));

        var salt = RandomNumberGenerator.GetBytes(SaltBytes);
        var nonce = RandomNumberGenerator.GetBytes(NonceBytes);
        var key = DeriveKey(passphrase, salt);

        var plain = Encoding.UTF8.GetBytes(plainText);
        var cipher = new byte[plain.Length];
        var tag = new byte[TagBytes];

        using (var aes = new AesGcm(key, TagBytes))
            aes.Encrypt(nonce, plain, cipher, tag);

        CryptographicOperations.ZeroMemory(key);

        return Prefix
            + Convert.ToBase64String(salt) + ":"
            + Convert.ToBase64String(nonce) + ":"
            + Convert.ToBase64String(tag) + ":"
            + Convert.ToBase64String(cipher);
    }

    public static string? Unprotect(string? value, string passphrase)
    {
        if (string.IsNullOrEmpty(value) || !IsProtected(value))
            return value;

        var parts = value[Prefix.Length..].Split(':');
        if (parts.Length != 4)
            throw new SecretProtectionException(
                "Der geschützte Wert ist unvollständig oder beschädigt.",
                new FormatException("Erwartet werden vier Abschnitte."));

        byte[] salt, nonce, tag, cipher;
        try
        {
            salt = Convert.FromBase64String(parts[0]);
            nonce = Convert.FromBase64String(parts[1]);
            tag = Convert.FromBase64String(parts[2]);
            cipher = Convert.FromBase64String(parts[3]);
        }
        catch (FormatException ex)
        {
            throw new SecretProtectionException("Der geschützte Wert ist beschädigt.", ex);
        }

        var key = DeriveKey(passphrase, salt);
        var plain = new byte[cipher.Length];

        try
        {
            using var aes = new AesGcm(key, TagBytes);
            aes.Decrypt(nonce, cipher, tag, plain);
        }
        catch (CryptographicException ex)
        {
            // AES-GCM erkennt sowohl eine falsche Passphrase als auch nachträgliche
            // Veränderung — beides landet hier.
            throw new SecretProtectionException(
                "Entschlüsselung fehlgeschlagen. Entweder ist die Passphrase falsch " +
                "oder die Sicherung wurde verändert.", ex);
        }
        finally
        {
            CryptographicOperations.ZeroMemory(key);
        }

        return Encoding.UTF8.GetString(plain);
    }

    private static byte[] DeriveKey(string passphrase, byte[] salt)
        => Rfc2898DeriveBytes.Pbkdf2(
            Encoding.UTF8.GetBytes(passphrase), salt, Iterations, HashAlgorithmName.SHA256, KeyBytes);
}
