import "server-only";
import {
  createCipheriv,
  createDecipheriv,
  hkdfSync,
  randomBytes,
  scryptSync,
  timingSafeEqual,
} from "node:crypto";

// ──────────────────────────────────────────────────────────────────────────
// Verschlüsselung des Passwortmanagers — zwei Schichten (Envelope):
//
//   1. Server-Schlüssel (VAULT_MASTER_KEY): schützt die Datenbank. Wer nur
//      einen DB-Dump hat, kann nichts lesen.
//   2. Optionale PIN-Schicht: der Wert wird zusätzlich mit einem aus der PIN
//      abgeleiteten Schlüssel verschlüsselt. Ohne PIN ist er dann selbst mit
//      Serverzugriff nicht lesbar — auch nicht für Admins.
//
// Verfahren: AES-256-GCM (authentifiziert, erkennt Manipulation), PIN-Ableitung
// per scrypt mit hohen Kosten. Eine 4–8-stellige PIN hat systembedingt wenig
// Entropie (10.000–100 Mio. Möglichkeiten); die Kostenparameter und die
// Fehlversuchssperre in `actions.ts` sind die eigentliche Bremse gegen Raten.
// ──────────────────────────────────────────────────────────────────────────

const ALGO = "aes-256-gcm";
const KEY_BYTES = 32;
const IV_BYTES = 12;
const SALT_BYTES = 16;

// scrypt-Kosten: ~100 ms und 32 MiB pro Versuch auf üblicher Hardware.
const SCRYPT = { N: 2 ** 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 } as const;

export const PIN_MIN_LENGTH = 4;
export const PIN_MAX_LENGTH = 8;

export function isValidPin(pin: string): boolean {
  return new RegExp(`^[0-9]{${PIN_MIN_LENGTH},${PIN_MAX_LENGTH}}$`).test(pin);
}

let cachedMasterKey: Buffer | null = null;

/**
 * Server-Hauptschlüssel. Bevorzugt `VAULT_MASTER_KEY` (32 Byte, base64 oder
 * hex). Fehlt er, wird ersatzweise ein Schlüssel aus `AUTH_SECRET` abgeleitet,
 * damit das Modul ohne Zusatzkonfiguration läuft.
 *
 * Achtung: Ändert sich die Quelle des Schlüssels, sind bestehende Einträge
 * nicht mehr entschlüsselbar. Für den Produktivbetrieb `VAULT_MASTER_KEY`
 * setzen und sichern.
 */
function masterKey(): Buffer {
  if (cachedMasterKey) return cachedMasterKey;

  const raw = process.env.VAULT_MASTER_KEY?.trim();
  if (raw) {
    const buf = /^[0-9a-fA-F]{64}$/.test(raw)
      ? Buffer.from(raw, "hex")
      : Buffer.from(raw, "base64");
    if (buf.length !== KEY_BYTES) {
      throw new Error(
        "VAULT_MASTER_KEY muss 32 Byte lang sein (base64 oder hex). Erzeugen mit: openssl rand -base64 32",
      );
    }
    cachedMasterKey = buf;
    return buf;
  }

  const fallback = process.env.AUTH_SECRET;
  if (!fallback) {
    throw new Error(
      "Weder VAULT_MASTER_KEY noch AUTH_SECRET gesetzt — der Passwortmanager kann nicht verschlüsseln.",
    );
  }
  cachedMasterKey = Buffer.from(
    hkdfSync("sha256", Buffer.from(fallback), Buffer.alloc(0), Buffer.from("avaria:vault:v1"), KEY_BYTES),
  );
  return cachedMasterKey;
}

/** Nur für Tests/Diagnose: zwischengespeicherten Schlüssel verwerfen. */
export function resetMasterKeyCache(): void {
  cachedMasterKey = null;
}

function encrypt(plain: string, key: Buffer): string {
  const iv = randomBytes(IV_BYTES);
  const cipher = createCipheriv(ALGO, key, iv);
  const data = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  return `v1.${iv.toString("base64")}.${tag.toString("base64")}.${data.toString("base64")}`;
}

function decrypt(payload: string, key: Buffer): string {
  const [version, iv, tag, data] = payload.split(".");
  if (version !== "v1" || !iv || !tag || !data) {
    throw new Error("Ungültiges Verschlüsselungsformat.");
  }
  const decipher = createDecipheriv(ALGO, key, Buffer.from(iv, "base64"));
  decipher.setAuthTag(Buffer.from(tag, "base64"));
  return Buffer.concat([
    decipher.update(Buffer.from(data, "base64")),
    decipher.final(),
  ]).toString("utf8");
}

function pinKey(pin: string, salt: Buffer): Buffer {
  return scryptSync(pin.normalize("NFKC"), salt, KEY_BYTES, SCRYPT);
}

/** Innere Hülle: das, was unter dem Server-Schlüssel liegt. */
type Envelope =
  | { v: 1; pin: false; value: string }
  | { v: 1; pin: true; salt: string; value: string; check: string };

/**
 * Verschlüsselt ein Geheimnis. Mit `pin` entsteht zusätzlich eine
 * PIN-Schicht — ohne diese PIN ist der Wert danach nicht mehr lesbar.
 */
export function sealSecret(plain: string, pin?: string | null): string {
  let envelope: Envelope;

  if (pin) {
    if (!isValidPin(pin)) {
      throw new Error(`PIN muss ${PIN_MIN_LENGTH}–${PIN_MAX_LENGTH} Ziffern haben.`);
    }
    const salt = randomBytes(SALT_BYTES);
    const key = pinKey(pin, salt);
    envelope = {
      v: 1,
      pin: true,
      salt: salt.toString("base64"),
      value: encrypt(plain, key),
      // Prüfwert, um eine falsche PIN sauber von einem defekten Datensatz zu
      // unterscheiden (der GCM-Tag allein schlägt in beiden Fällen fehl).
      check: encrypt("ok", key),
    };
  } else {
    envelope = { v: 1, pin: false, value: plain };
  }

  return encrypt(JSON.stringify(envelope), masterKey());
}

export type OpenResult =
  | { ok: true; value: string }
  | { ok: false; reason: "PIN_REQUIRED" | "PIN_WRONG" | "CORRUPT" };

/** Entschlüsselt ein Geheimnis; PIN-geschützte Einträge brauchen die PIN. */
export function openSecret(stored: string, pin?: string | null): OpenResult {
  let envelope: Envelope;
  try {
    envelope = JSON.parse(decrypt(stored, masterKey())) as Envelope;
  } catch {
    return { ok: false, reason: "CORRUPT" };
  }

  if (!envelope.pin) return { ok: true, value: envelope.value };
  if (!pin || !isValidPin(pin)) return { ok: false, reason: "PIN_REQUIRED" };

  const key = pinKey(pin, Buffer.from(envelope.salt, "base64"));
  try {
    const check = Buffer.from(decrypt(envelope.check, key));
    if (check.length !== 2 || !timingSafeEqual(check, Buffer.from("ok"))) {
      return { ok: false, reason: "PIN_WRONG" };
    }
    return { ok: true, value: decrypt(envelope.value, key) };
  } catch {
    return { ok: false, reason: "PIN_WRONG" };
  }
}

/** Ist der Eintrag PIN-geschützt? (ohne das Geheimnis zu entschlüsseln) */
export function isPinSealed(stored: string): boolean {
  try {
    return (JSON.parse(decrypt(stored, masterKey())) as Envelope).pin === true;
  } catch {
    return false;
  }
}
