using System.Security.Cryptography;
using ClawdDotNet.Core.Storage;

namespace ClawdDotNet.Core.Security;

/// <summary>
/// Verwaltet den lokalen Schlüssel, mit dem <see cref="SecretProtector"/> die
/// <c>enc:v2</c>-Werte sichert.
///
/// <para><b>Ein Schlüssel je Benutzer und Rechner.</b> Er liegt als
/// <c>secret.key</c> in <see cref="AppPaths.ConfigDirectory"/> — bewusst außerhalb
/// des Instanzverzeichnisses, damit eine Sicherung der Instanz ihn nicht mitnimmt.
/// Das hält die Schutzstufe, die DPAPI zuvor bot: Die Konfigurationsdatei allein
/// nützt auf einem anderen Rechner nichts.</para>
///
/// <para><b>Rechte.</b> Unter Unix <c>0600</c>. Unter Windows erbt die Datei die
/// Rechte des Benutzerprofils und wird zusätzlich per DPAPI gesichert — dort bleibt
/// die Bindung an das Benutzerkonto also erhalten, obwohl das Format
/// plattformübergreifend ist.</para>
/// </summary>
public static class SecretKeyStore
{
    private const string KeyFileName = "secret.key";
    private const int KeySize = 32; // AES-256

    /// <summary>DPAPI-Zusatzkontext für die Schlüsseldatei (nur Windows).</summary>
    private static readonly byte[] KeyEntropy =
        System.Text.Encoding.UTF8.GetBytes("ClawdDotNet.SecretKey.v2");

    private static readonly Lock Gate = new();
    private static byte[]? _cached;
    private static string? _overrideDirectory;

    public static string KeyFilePath => Path.Combine(Directory(), KeyFileName);

    /// <summary>
    /// Verlegt den Schlüssel — für Tests, damit sie den echten Benutzerschlüssel weder
    /// lesen noch überschreiben.
    /// </summary>
    public static void UseDirectory(string? directory)
    {
        lock (Gate)
        {
            _overrideDirectory = directory;
            _cached = null;
        }
    }

    /// <summary>
    /// Liest den Schlüssel oder legt ihn beim ersten Aufruf an.
    /// </summary>
    /// <exception cref="IOException">Wenn das Verzeichnis nicht beschreibbar ist.</exception>
    public static byte[] GetOrCreateKey()
    {
        lock (Gate)
        {
            if (_cached is not null) return _cached;

            var directory = AppPaths.EnsureDirectory(Directory());
            var path = Path.Combine(directory, KeyFileName);

            if (File.Exists(path))
            {
                var stored = Unwrap(File.ReadAllBytes(path));
                if (stored.Length == KeySize)
                {
                    _cached = stored;
                    return _cached;
                }

                // Eine Datei falscher Länge ist kaputt. Sie stillschweigend zu ersetzen
                // würde alle bestehenden Werte unlesbar machen, ohne dass jemand erfährt,
                // warum — deshalb hier abbrechen und den Pfad nennen.
                throw new CryptographicException(
                    $"Die Schlüsseldatei {path} ist beschädigt ({stored.Length} statt {KeySize} Byte). "
                    + "Sie darf nicht ersetzt werden, ohne die verschlüsselten Werte neu zu setzen.");
            }

            var key = RandomNumberGenerator.GetBytes(KeySize);

            // Über AtomicFile, damit kein halb geschriebener Schlüssel entsteht — der
            // würde alle Geheimnisse dieser Installation unlesbar machen.
            AtomicFile.WriteAllBytes(path, Wrap(key));
            AppPaths.RestrictToOwner(path);

            _cached = key;
            return _cached;
        }
    }

    private static string Directory() => _overrideDirectory ?? AppPaths.ConfigDirectory;

    /// <summary>Unter Windows zusätzlich per DPAPI an das Benutzerkonto binden.</summary>
    private static byte[] Wrap(byte[] key)
        => OperatingSystem.IsWindows() ? ProtectWithDpapi(key) : key;

    private static byte[] Unwrap(byte[] stored)
        => OperatingSystem.IsWindows() ? UnprotectWithDpapi(stored) : stored;

    [System.Runtime.Versioning.SupportedOSPlatform("windows")]
    private static byte[] ProtectWithDpapi(byte[] key)
        => ProtectedData.Protect(key, KeyEntropy, DataProtectionScope.CurrentUser);

    [System.Runtime.Versioning.SupportedOSPlatform("windows")]
    private static byte[] UnprotectWithDpapi(byte[] stored)
        => ProtectedData.Unprotect(stored, KeyEntropy, DataProtectionScope.CurrentUser);
}
