using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using ClawdDotNet.Core.Storage;

namespace ClawdDotNet.Core.Security;

/// <summary>
/// Verschlüsselt Zugangsdaten in Konfigurationsdateien.
///
/// Hintergrund (S7): OpenRouter-Schlüssel, Datenbank-Verbindungszeichenfolgen samt
/// Passwort, Mail-Zugangsdaten und das Telegram-2FA-Passwort lagen im Klartext in
/// AgentSettings.json und InstanceConfig.json. Wer die Dateien lesen konnte — ein
/// Backup, eine Dateifreigabe, ein versehentlicher Commit — hatte alle Zugänge.
///
/// <para><b>Zwei Formate.</b></para>
///
/// <list type="bullet">
/// <item><c>enc:v1:</c> — DPAPI im Benutzerkontext. Nur unter Windows lesbar. Wird
///   nicht mehr geschrieben, aber weiterhin gelesen: bestehende Installationen sollen
///   ohne Zutun weiterlaufen und wandern beim nächsten Speichern von selbst auf v2.</item>
/// <item><c>enc:v2:</c> — AES-256-GCM mit einem Schlüssel aus
///   <see cref="AppPaths.ConfigDirectory"/>. Läuft auf jeder Plattform.</item>
/// </list>
///
/// <para><b>Warum v2 überhaupt nötig wurde.</b> Die vorige Fassung gab unter Linux
/// stillschweigend den Klartext zurück — <c>Protect</c> verschlüsselte dort schlicht
/// nicht. Auf einem Server, der per SSH erreichbar ist und gesichert wird, wäre das
/// schlechter gewesen als auf einem Einzelplatz-Windows.</para>
///
/// <para><b>Schutzstufe.</b> Dieselbe wie DPAPI zuvor: gegen Weitergabe der
/// Konfigurationsdatei, gegen ein Backup, gegen einen versehentlichen Commit — nicht
/// gegen einen Angreifer, der bereits als dieser Benutzer läuft. Der Schlüssel liegt
/// deshalb bewusst <b>außerhalb</b> des Instanzverzeichnisses: Eine Sicherung der
/// Instanz enthält ihn nicht, und ob Geheimnisse mitreisen, entscheidet weiterhin
/// allein die Sicherungsrichtlinie in <c>BackupService</c>.</para>
/// </summary>
public static class SecretProtector
{
    private const string PrefixV1 = "enc:v1:";
    private const string PrefixV2 = "enc:v2:";

    /// <summary>Zusätzlicher Kontext, damit ein Wert nicht in anderem Zusammenhang wiederverwendbar ist.</summary>
    private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("ClawdDotNet.Secrets.v1");

    /// <summary>Wird v2 als Zusatzangabe mitgeschrieben und beim Entschlüsseln geprüft.</summary>
    private static readonly byte[] AssociatedData = Encoding.UTF8.GetBytes("ClawdDotNet.Secrets.v2");

    private const int NonceSize = 12;  // AES-GCM: vorgeschriebene Länge
    private const int TagSize = 16;

    public static bool IsProtected(string? value)
        => value is not null
           && (value.StartsWith(PrefixV2, StringComparison.Ordinal)
               || value.StartsWith(PrefixV1, StringComparison.Ordinal));

    /// <summary>
    /// Verschlüsselt einen Wert. Bereits verschlüsselte und leere Werte bleiben unverändert,
    /// damit die Funktion gefahrlos mehrfach angewendet werden kann.
    /// </summary>
    public static string? Protect(string? plainText)
    {
        if (string.IsNullOrEmpty(plainText) || IsProtected(plainText))
            return plainText;

        try
        {
            var key = SecretKeyStore.GetOrCreateKey();

            var nonce = RandomNumberGenerator.GetBytes(NonceSize);
            var plain = Encoding.UTF8.GetBytes(plainText);
            var cipher = new byte[plain.Length];
            var tag = new byte[TagSize];

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

            // nonce ‖ tag ‖ ciphertext — feste Längen vorn, damit das Zerlegen eindeutig ist.
            var payload = new byte[NonceSize + TagSize + cipher.Length];
            nonce.CopyTo(payload, 0);
            tag.CopyTo(payload, NonceSize);
            cipher.CopyTo(payload, NonceSize + TagSize);

            return PrefixV2 + Convert.ToBase64String(payload);
        }
        catch (Exception ex) when (ex is CryptographicException or IOException or UnauthorizedAccessException)
        {
            // Kein Schlüssel anlegbar (etwa ein schreibgeschütztes Konfigurationsverzeichnis).
            // Den Wert unverschlüsselt zu speichern wäre die stille Rückkehr zu genau dem
            // Zustand, den S7 behoben hat — deshalb hier abbrechen statt weiterreichen.
            throw new SecretProtectionException(
                "Ein Wert konnte nicht verschlüsselt werden, weil der lokale Schlüssel nicht "
                + $"lesbar oder anlegbar ist ({SecretKeyStore.KeyFilePath}). Ohne ihn würden "
                + "Zugangsdaten im Klartext gespeichert — der Vorgang wurde abgebrochen.", ex);
        }
    }

    /// <summary>
    /// Entschlüsselt einen Wert. Klartext aus älteren Konfigurationen wird unverändert
    /// zurückgegeben — so bleiben bestehende Installationen lauffähig.
    /// </summary>
    public static string? Unprotect(string? value)
    {
        if (string.IsNullOrEmpty(value)) return value;

        if (value.StartsWith(PrefixV2, StringComparison.Ordinal))
            return UnprotectV2(value[PrefixV2.Length..]);

        if (value.StartsWith(PrefixV1, StringComparison.Ordinal))
            return UnprotectV1(value[PrefixV1.Length..]);

        return value; // Klartext aus älteren Konfigurationen
    }

    private static string UnprotectV2(string payload)
    {
        try
        {
            var raw = Convert.FromBase64String(payload);
            if (raw.Length < NonceSize + TagSize)
                throw new CryptographicException("Der verschlüsselte Block ist unvollständig.");

            var key = SecretKeyStore.GetOrCreateKey();

            var nonce = raw.AsSpan(0, NonceSize);
            var tag = raw.AsSpan(NonceSize, TagSize);
            var cipher = raw.AsSpan(NonceSize + TagSize);
            var plain = new byte[cipher.Length];

            using (var aes = new AesGcm(key, TagSize))
                aes.Decrypt(nonce, cipher, tag, plain, AssociatedData);

            return Encoding.UTF8.GetString(plain);
        }
        catch (Exception ex) when (ex is CryptographicException or FormatException
                                       or IOException or UnauthorizedAccessException)
        {
            throw new SecretProtectionException(
                "Ein verschlüsselter Wert konnte nicht gelesen werden. Das passiert, wenn die "
                + "Konfiguration von einem anderen Rechner oder Benutzer stammt — der Schlüssel "
                + $"dazu liegt in {SecretKeyStore.KeyFilePath} und reist nicht mit. "
                + "Bitte den betroffenen Wert in den Einstellungen neu eintragen.", ex);
        }
    }

    private static string UnprotectV1(string payload)
    {
        if (!OperatingSystem.IsWindows())
        {
            // Der Fall beim Umzug einer Windows-Instanz auf Linux. Ihn als Klartext
            // durchzureichen wäre falsch — dann ginge ein unbrauchbarer Schlüssel an die API.
            throw new SecretProtectionException(
                "Dieser Wert wurde mit der Windows-Verschlüsselung (DPAPI) gesichert und lässt "
                + "sich hier nicht lesen. Beim Umzug einer Instanz von Windows müssen die "
                + "betroffenen Werte einmal neu eingetragen werden; danach liegen sie im "
                + "plattformübergreifenden Format vor.",
                new PlatformNotSupportedException("DPAPI ist nur unter Windows verfügbar."));
        }

        try
        {
            return Encoding.UTF8.GetString(UnprotectWindows(Convert.FromBase64String(payload)));
        }
        catch (Exception ex) when (ex is CryptographicException or FormatException)
        {
            throw new SecretProtectionException(
                "Ein verschlüsselter Wert konnte nicht gelesen werden. Das passiert, wenn die "
                + "Konfiguration von einem anderen Windows-Benutzer oder Rechner stammt. "
                + "Bitte den betroffenen Wert in den Einstellungen neu eintragen.", ex);
        }
    }

    [SupportedOSPlatform("windows")]
    private static byte[] UnprotectWindows(byte[] data)
        => ProtectedData.Unprotect(data, Entropy, DataProtectionScope.CurrentUser);
}

public sealed class SecretProtectionException(string message, Exception inner)
    : Exception(message, inner);
