import { Response } from 'express';
import { AuthRequest } from '../types/index.js';
import * as auditService from '../services/audit.service.js';
import { logChange } from '../services/audit.service.js';
import { AuditAction, AuditSensitivity } from '@prisma/client';
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';

/**
 * Filterwerte aus der Query pruefen (Pentest R185-02).
 *
 * Vorher gingen `action` und `sensitivity` als roher String an die Enum-Spalte.
 * Ein ungueltiger Wert liess Prisma auflaufen und der Handler antwortete 500.
 * Das ist zweierlei: eine fehlende Validierung – und ein Fehler-Orakel. Wer
 * 200 gegen 500 vergleicht, liest die Enum-Mitglieder aus, ohne sie zu kennen.
 * Ein ungueltiger Filter ist eine schlechte ANFRAGE, keine Server-Panne: 400.
 *
 * Dasselbe gilt fuer Datumsangaben (`new Date('foo')` ergibt Invalid Date und
 * sprengt die Query erst in der Datenbank) und fuer Zahlen (`parseInt('x')`
 * ergibt NaN).
 */
class FilterFehler extends Error {}

const AUDIT_ACTIONS = Object.values(AuditAction) as string[];
const AUDIT_SENSITIVITIES = Object.values(AuditSensitivity) as string[];

function pruefeEnum<T extends string>(
  wert: unknown, erlaubt: string[], feld: string,
): T | undefined {
  if (wert === undefined || wert === '') return undefined;
  if (typeof wert !== 'string' || !erlaubt.includes(wert)) {
    // Die erlaubten Werte stehen ohnehin in der Oberflaeche und im Schema –
    // sie zu nennen verraet nichts und erspart Rateversuche.
    throw new FilterFehler(
      `Ungültiger Wert für "${feld}". Erlaubt: ${erlaubt.join(', ')}.`,
    );
  }
  return wert as T;
}

function pruefeDatum(wert: unknown, feld: string): Date | undefined {
  if (wert === undefined || wert === '') return undefined;
  const d = new Date(wert as string);
  if (Number.isNaN(d.getTime())) {
    throw new FilterFehler(`Ungültiges Datum für "${feld}".`);
  }
  return d;
}

function pruefeZahl(wert: unknown, feld: string): number | undefined {
  if (wert === undefined || wert === '') return undefined;
  const n = Number(wert);
  if (!Number.isInteger(n) || n < 0) {
    throw new FilterFehler(`Ungültige Zahl für "${feld}".`);
  }
  return n;
}

/** Freitextfelder begrenzen – unbegrenzte LIKE-Muster sind teuer. */
function pruefeText(wert: unknown, feld: string, maxLaenge = 200): string | undefined {
  if (wert === undefined || wert === '') return undefined;
  if (typeof wert !== 'string' || wert.length > maxLaenge) {
    throw new FilterFehler(`Ungültiger Wert für "${feld}" (max. ${maxLaenge} Zeichen).`);
  }
  return wert;
}

/**
 * Filter aus der Query lesen – EINE Stelle fuer Liste und Export (R186-01).
 *
 * Vorher pflegte jeder Endpunkt seine eigene Liste, und die des Exports war
 * kuerzer: `userId`, `customerId`, `dataSubjectId`, `resourceId`, `success` und
 * `search` wurden dort stillschweigend verworfen. Ein bewusst eingegrenzter
 * Export – „nur die Spur von Benutzer X“ fuer eine DSGVO-Auskunft oder eine
 * Innentaeter-Pruefung – lieferte damit das VOLLSTAENDIGE Protokoll aller
 * Nutzer zurueck. Mit HTTP 200 und ohne jeden Hinweis: ein beruhigendes
 * Signal ueber einem Ergebnis, das genau das Gegenteil dessen ist, was
 * angefragt wurde. Auf einem datenminimierungspflichtigen Export ist das
 * nicht nur ein fehlender Filter, sondern eine Weitergabe.
 *
 * Zwei Listen, die dasselbe bedeuten sollen, laufen frueher oder spaeter
 * auseinander. Deshalb gibt es jetzt nur noch diese eine.
 */
function leseFilter(req: AuthRequest) {
  const q = req.query;
  return {
    userId: pruefeZahl(q.userId, 'userId'),
    customerId: pruefeZahl(q.customerId, 'customerId'),
    dataSubjectId: pruefeZahl(q.dataSubjectId, 'dataSubjectId'),
    action: pruefeEnum<AuditAction>(q.action, AUDIT_ACTIONS, 'action'),
    sensitivity: pruefeEnum<AuditSensitivity>(q.sensitivity, AUDIT_SENSITIVITIES, 'sensitivity'),
    resourceType: pruefeText(q.resourceType, 'resourceType', 100),
    resourceId: pruefeText(q.resourceId, 'resourceId', 100),
    startDate: pruefeDatum(q.startDate, 'startDate'),
    endDate: pruefeDatum(q.endDate, 'endDate'),
    success: q.success !== undefined ? q.success === 'true' : undefined,
    search: pruefeText(q.search, 'search'),
  };
}

/**
 * Audit-Logs mit Filtern abrufen
 */
export async function getAuditLogs(req: AuthRequest, res: Response) {
  try {
    const result = await auditService.searchAuditLogs({
      ...leseFilter(req),
      page: pruefeZahl(req.query.page, 'page') || 1,
      // Deckel: sonst laesst sich ueber `limit` die gesamte Tabelle in einem
      // Zug ziehen, an der Seitenlogik vorbei.
      limit: Math.min(pruefeZahl(req.query.limit, 'limit') || 50, 200),
    });

    res.json({ success: true, ...result });
  } catch (error) {
    if (error instanceof FilterFehler) {
      res.status(400).json({ success: false, error: error.message });
      return;
    }
    console.error('Fehler beim Abrufen der Audit-Logs:', error);
    res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' });
  }
}

/**
 * Einzelnes Audit-Log abrufen
 */
export async function getAuditLogById(req: AuthRequest, res: Response) {
  try {
    const id = parseInt(req.params.id);
    // Nicht-numerische :id (z.B. GET /audit-logs/verify statt POST /verify)
    // landete sonst als NaN in Prisma findUnique → 500. Sauberer 400.
    // (Pentest R120 – gleiche „400 statt 500"-Klasse wie R64.1/R104.1.)
    if (Number.isNaN(id)) {
      return res.status(400).json({ success: false, error: 'Ungültige Audit-Log-ID' });
    }
    const log = await auditService.getAuditLogById(id);

    if (!log) {
      return res.status(404).json({ success: false, error: 'Audit-Log nicht gefunden' });
    }

    res.json({ success: true, data: log });
  } catch (error) {
    console.error('Fehler beim Abrufen des Audit-Logs:', error);
    res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Audit-Logs' });
  }
}

/**
 * Audit-Logs für einen Kunden abrufen (DSGVO)
 */
export async function getAuditLogsByCustomer(req: AuthRequest, res: Response) {
  try {
    const customerId = parseInt(req.params.customerId);
    if (Number.isNaN(customerId)) {
      return res.status(400).json({ success: false, error: 'Ungültige Kunden-ID' });
    }
    const logs = await auditService.getAuditLogsByDataSubject(customerId);

    res.json({ success: true, data: logs });
  } catch (error) {
    console.error('Fehler beim Abrufen der Kunden-Audit-Logs:', error);
    res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' });
  }
}

/**
 * Audit-Logs exportieren
 */
export async function exportAuditLogs(req: AuthRequest, res: Response) {
  try {
    const format = req.query.format === 'csv' ? 'csv' : 'json';
    const content = await auditService.exportAuditLogs(leseFilter(req), format);

    if (format === 'csv') {
      const filename = `audit-logs-${new Date().toISOString().split('T')[0]}.csv`;
      res.setHeader('Content-Type', 'text/csv; charset=utf-8');
      res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
      // BOM für Excel UTF-8 Erkennung
      res.send('\uFEFF' + content);
    } else {
      res.json({ success: true, data: JSON.parse(content) })
    }
  } catch (error) {
    if (error instanceof FilterFehler) {
      res.status(400).json({ success: false, error: error.message });
      return;
    }
    console.error('Fehler beim Exportieren der Audit-Logs:', error);
    res.status(500).json({ success: false, error: 'Fehler beim Exportieren' });
  }
}

/**
 * Hash-Ketten-Integrität prüfen
 */
export async function verifyIntegrity(req: AuthRequest, res: Response) {
  try {
    const { fromId, toId } = req.query;

    const result = await auditService.verifyIntegrity(
      fromId ? parseInt(fromId as string) : undefined,
      toId ? parseInt(toId as string) : undefined
    );

    // Zwei sehr unterschiedliche Befunde sauber trennen – vorher wurde beides
    // pauschal als "manipuliert" gemeldet, was strukturelle Lücken wie einen
    // echten Angriff aussehen liess (und damit die Meldung entwertete).
    const tampered = result.tamperedEntries.length;
    const gaps = result.chainGaps.length;
    const unexplained = result.unexplainedGaps.length;
    // Beglaubigte Alt-Luecken sind kein offener Befund mehr, verschwinden aber
    // auch nicht aus dem Bericht - sie werden eigens benannt.
    const beglaubigt = result.attestedGaps.length;
    const offeneGaps = gaps - beglaubigt;
    const offeneUnexplained = result.unexplainedGaps.filter(
      (id) => !result.attestedGaps.includes(id),
    ).length;

    const luecken = offeneGaps > 0
      ? `${offeneGaps} strukturelle Lücke${offeneGaps === 1 ? '' : 'n'}` +
        (offeneUnexplained === 0
          ? ' (alle durch protokollierte Löschungen erklärt)'
          : offeneUnexplained < offeneGaps
            ? `, davon ${offeneUnexplained} ohne dokumentierte Löschung`
            : ' ohne dokumentierte Löschung')
      : '';

    // Der Satz erscheint IMMER, wenn beglaubigte Luecken existieren - auch
    // neben einem Befund. Wer den Bericht liest, soll nie den Eindruck
    // bekommen, die Kette sei lueckenlos, wenn sie es nicht ist.
    const weitere = tampered > 0 || offeneGaps > 0 ? 'weitere ' : '';
    const beglaubigtText =
      beglaubigt === 0
        ? ''
        : beglaubigt === 1
          ? ` Eine ${weitere}Lücke stammt aus der Zeit vor dem Bestandssiegel und ist darin als ` +
            `Vorbefund beglaubigt (ID ${result.attestedGaps[0]}); der betroffene Eintrag selbst ` +
            'ist unverändert.'
          : ` ${beglaubigt} ${weitere}Lücken stammen aus der Zeit vor dem Bestandssiegel und sind ` +
            `darin als Vorbefund beglaubigt (IDs ${result.attestedGaps.join(', ')}); ` +
            'die betroffenen Einträge selbst sind unverändert.';

    const unverifiable = result.unverifiableEntries.length;
    const keinSchluessel = unverifiable > 0
      ? ` ${unverifiable} Einträge sind HMAC-signiert und ohne konfigurierten AUDIT_HMAC_KEY nicht prüfbar.`
      : '';

    const siegel =
      result.backlogSealStatus === 'gebrochen'
        ? ' ⚠ Bestandssiegel GEBROCHEN:' +
          (result.backlogTampered.length
            ? ` ${result.backlogTampered.length} Alteinträge nachträglich verändert.`
            : '') +
          (result.backlogMissing.length
            ? ` ${result.backlogMissing.length} gesiegelte Einträge wurden ENTFERNT (${result.backlogMissing.join(', ')}).`
            : '') +
          (!result.backlogTampered.length && !result.backlogMissing.length
            ? ' Der Blattbestand des Siegels stimmt nicht mehr mit der signierten Wurzel überein.'
            : '')
        : result.backlogSealStatus === 'entfernt'
          ? ' ⚠ Bestandssiegel ENTFERNT: Es liegen versiegelte Blattwerte vor, aber kein gültiger ' +
            'Siegel-Marker mehr. Der Marker wurde gelöscht oder unbrauchbar gemacht – Änderungen am ' +
            'Altbestand wären dadurch wieder unsichtbar. Das ist KEIN Normalzustand.'
          : result.backlogSealStatus === 'leer'
            ? ' Hinweis: Es besteht ein Bestandssiegel, das aber NICHTS umschließt – ' +
              'zum Zeitpunkt des Siegelns gab es keine unsignierten Alteinträge. Es sichert ' +
              'also nichts ab. Das ist kein Fehler, aber auch keine Zusage.'
          : result.backlogSealStatus === 'nicht_noetig'
            ? ' Ein Bestandssiegel wird hier nicht gebraucht: Es gibt keine unsignierten Alteinträge.'
            : result.backlogSealStatus === 'kein_siegel'
              ? ' Hinweis: Der Altbestand ist nicht versiegelt – Änderungen daran wären nicht erkennbar. ' +
                'Behebbar mit POST /api/audit-logs/seal-backlog {"confirm":"SEAL"}.'
              : '';

    // Eine Neuberechnung der Kette gehoert IMMER erwaehnt – auch und gerade,
    // wenn sonst alles grün ist. Nach einem Rehash ist die Kette
    // zwangslaeufig stimmig, auch ueber Loeschungen hinweg. „Lueckenlos
    // verkettet“ heisst dann nur noch „seit dem Rehash“, und wer das nicht
    // mitliest, nimmt eine Entwarnung mit, die es so nicht gibt.
    const rehashText = (() => {
      if (result.rehashes.length === 0) return '';
      const letzter = result.rehashes[result.rehashes.length - 1];
      const datum = new Date(letzter.zeitpunkt).toLocaleString('de-DE', {
        dateStyle: 'short',
        timeStyle: 'short',
      });
      const wieOft =
        result.rehashes.length === 1
          ? 'Die Kette wurde einmal neu berechnet'
          : `Die Kette wurde ${result.rehashes.length}× neu berechnet, zuletzt`;
      // Nur benennen, was es zu benennen gibt. Bei sauberem Vorzustand ist die
      // Auskunft „nichts uebertuencht“ selbst eine nuetzliche Information –
      // ein Rehash ueber einer unbeanstandeten Kette wiegt anders als einer
      // ueber 700 Luecken.
      const vb = letzter.vorbefund;
      const uebertuencht = !vb
        ? ' Der Zustand vor dieser Neuberechnung ist nicht mehr feststellbar.'
        : vb.manipuliert === 0 && vb.luecken === 0
          ? ' Die Kette war unmittelbar davor unbeanstandet – es wurde nichts überdeckt.'
          : ' Unmittelbar davor: ' +
            `${vb.manipuliert} beanstandete${vb.manipuliert === 1 ? 'r Eintrag' : ' Einträge'}` +
            ` und ${vb.luecken} Lücke${vb.luecken === 1 ? '' : 'n'}. ` +
            'Diese Spuren sind seitdem nicht mehr in der Kette sichtbar, sondern nur noch im ' +
            `Vorbefund des Rehash-Eintrags (id ${letzter.id}).`;
      const ohneSignatur = result.rehashes.some((r) => !r.signiert)
        ? ' Achtung: Mindestens ein Rehash-Eintrag trägt keine gültige Signatur.'
        : '';
      return (
        ` ⚠ ${wieOft} am ${datum}` +
        (letzter.neuBerechnet !== null ? ` (${letzter.neuBerechnet} Einträge)` : '') +
        '. Ein Rehash verknüpft alle Einträge neu – die Aussage dieser Prüfung ' +
        'reicht deshalb nur bis dorthin zurück, nicht weiter.' +
        uebertuencht +
        ohneSignatur
      );
    })();

    // Erneutes Siegeln kann legitim sein, verdient aber einen Blick: es
    // ersetzt die zuvor beglaubigte Wurzel (Pentest R173-03).
    const mehrfach =
      result.backlogSealCount > 1
        ? ` Hinweis: Der Altbestand wurde ${result.backlogSealCount}× versiegelt – der jüngste Marker gilt. ` +
          'Jeder Marker enthält den Befund vor dem Siegeln; bei Zweifeln dort vergleichen.'
        : '';

    // Bei problematischem Siegel darf KEIN beruhigender Einstiegssatz stehen –
    // sonst entwertet er die nachfolgende Warnung (Pentest R174-01).
    const siegelProblem =
      result.backlogSealStatus === 'gebrochen' || result.backlogSealStatus === 'entfernt';

    // „Keine Manipulation“ darf NICHT dastehen, solange `valid` falsch ist
    // (Pentest R183-02). Nach einem Cleanup mit abgesenkter Aufbewahrung waren
    // tausende Anmeldeprotokolle endgueltig geloescht – die Luecken durch
    // Tombstones „erklaert“, die Meldung las sich beruhigend, und der Befund
    // stand nur noch im Feld `valid`. Wer die Prosa liest statt des Felds,
    // klickt genau das weg, was ihn haette warnen sollen.
    const message = tampered > 0
      ? `${tampered} MANIPULIERTE Einträge gefunden` + (offeneGaps > 0 ? ` (zusätzlich ${luecken})` : '')
      : siegelProblem
        ? 'Die Kette selbst ist rechnerisch stimmig, ABER:'
        : offeneGaps > 0
          ? `Die Kette ist nicht mehr lückenlos: ${luecken}. Die verbliebenen Inhalte sind ` +
            'unverändert – aber gelöschte Einträge lassen sich naturgemäß nicht mehr prüfen. ' +
            'Dokumentierte Löschungen sind erwartbar; unerwartete gehören nachgegangen.'
          : beglaubigt > 0
            ? 'Alle Einträge sind unverändert. Seit dem Bestandssiegel ist keine neue Lücke ' +
              'entstanden.'
            : result.rehashes.length > 0
              ? 'Alle Einträge sind unverändert und lückenlos verkettet – allerdings erst ' +
                'seit der letzten Neuberechnung.'
              : 'Alle Einträge sind unverändert und lückenlos verkettet.';

    res.json({
      success: true,
      data: {
        valid: result.valid,
        checkedCount: result.checkedCount,
        invalidEntries: result.invalidEntries,
        // Ernst: Inhalt einer bestehenden Zeile wurde nachträglich verändert.
        tamperedEntries: result.tamperedEntries,
        // Meist harmlos: Verkettung unterbrochen, Inhalte selbst unversehrt.
        chainGaps: result.chainGaps,
        // Nur Lücken ohne protokollierte Löschung sind erklärungsbedürftig.
        unexplainedGaps: result.unexplainedGaps,
        // Alt-Lücken, die das Bestandssiegel als bereits vorhanden beglaubigt.
        attestedGaps: result.attestedGaps,
        // Protokollierte Neuberechnungen – begrenzen die Reichweite der Aussage.
        rehashes: result.rehashes,
        tampered: tampered > 0,
        message: message + beglaubigtText + rehashText + keinSchluessel + siegel + mehrfach,
        unverifiableEntries: result.unverifiableEntries,
        // Zustand des Bestandssiegels ueber den nicht signierbaren Altbestand.
        backlogSealStatus: result.backlogSealStatus,
        backlogTampered: result.backlogTampered,
        backlogMissing: result.backlogMissing,
        backlogSealCount: result.backlogSealCount,
      },
    });
  } catch (error) {
    console.error('Fehler bei der Integritätsprüfung:', error);
    res.status(500).json({ success: false, error: 'Fehler bei der Integritätsprüfung' });
  }
}

/**
 * Hash-Kette reparieren (alle Hashes neu berechnen)
 */
export async function rehashAll(req: AuthRequest, res: Response) {
  try {
    // Ausdrueckliche Bestaetigung verlangen (Pentest R170-01).
    //
    // Ein Rehash macht die Kette rechnerisch stimmig und setzt damit die
    // Beweiskraft der Vergangenheit zurueck - das darf kein Nebeneffekt eines
    // versehentlichen oder tastenden POST sein. Genau so wurde der Endpunkt
    // bei einer Methoden-Erkundung unbeabsichtigt ausgeloest.
    if (req.body?.confirm !== 'REHASH') {
      res.status(400).json({
        success: false,
        error:
          'Rehash setzt die Beweiskraft der bestehenden Einträge zurück und ist nicht ' +
          'umkehrbar. Zum Bestätigen {"confirm":"REHASH"} mitsenden.',
      });
      return;
    }

    const result = await auditService.rehashAll({
      userEmail: req.user?.email,
      ipAddress: req.ip || (req.socket as any)?.remoteAddress,
    });
    res.json({
      success: true,
      data: result,
      message: `${result.rehashedCount} Einträge neu gehasht. Kette ist jetzt intakt.`,
    });
  } catch (error) {
    console.error('Fehler beim Re-Hashing:', error);
    res.status(500).json({ success: false, error: 'Fehler beim Re-Hashing' });
  }
}

/**
 * Kontrollwert fuer das externe Gegenbuch (Notarisierung).
 *
 * Bewusst nur lesend und ohne Geheimnisse - die Gegenstelle braucht keinerlei
 * Schreibrecht auf dieses System, und dieses System kennt den Schluessel der
 * Gegenstelle nicht. Genau diese Trennung ist der Schutz.
 */
export async function getCheckpoint(req: AuthRequest, res: Response) {
  try {
    const roh = req.query.atId as string | undefined;
    const atId = roh !== undefined ? parseInt(roh, 10) : undefined;
    if (roh !== undefined && Number.isNaN(atId as number)) {
      res.status(400).json({ success: false, error: 'atId muss eine Zahl sein' });
      return;
    }
    const data = await auditService.getCheckpoint(atId);
    res.json({ success: true, data });
  } catch (error) {
    console.error('Fehler beim Erstellen des Kontrollwerts:', error);
    res.status(500).json({ success: false, error: 'Fehler beim Erstellen des Kontrollwerts' });
  }
}

/**
 * Altbestand versiegeln (Pentest R171-02).
 *
 * Einmalige, NICHT destruktive Aktion: schreibt nichts um, sondern haelt den
 * Ist-Inhalt der nicht signierbaren Alteintraege fest.
 */
export async function sealBacklog(req: AuthRequest, res: Response) {
  try {
    // Erst nachsehen, ob schon ein Siegel steht (Pentest R185-01).
    //
    // Ein zweites Siegeln ist etwas grundlegend anderes als das erste: Es
    // ERSETZT die Grundlage, gegen die Manipulation nachgewiesen wird. Wer den
    // Altbestand per Datenbankzugriff beschneidet und danach neu siegelt,
    // bekommt eine passende Wurzel und eine ueber den neuen Vorbefund
    // beglaubigte Luecke - und `valid` steht wieder auf `true`. Deshalb
    // verlangt das Ersetzen ein eigenes Wort und nicht dasselbe wie das
    // Einrichten.
    const vorher = await auditService.verifyIntegrity();
    const siegelSteht =
      vorher.backlogSealStatus === 'intakt' ||
      vorher.backlogSealStatus === 'gebrochen' ||
      vorher.backlogSealStatus === 'entfernt';

    const erwartet = siegelSteht ? 'RESEAL' : 'SEAL';
    if (req.body?.confirm !== erwartet) {
      res.status(400).json({
        success: false,
        error: siegelSteht
          ? 'Es besteht bereits ein Bestandssiegel. Erneutes Siegeln ERSETZT die ' +
            'bisherige Beweisgrundlage: bestehende Lücken werden neu beglaubigt und ' +
            'ein Befund am Altbestand verschwindet aus der Prüfung. Das ist kein ' +
            'Wartungsschritt. Zum Bestätigen {"confirm":"RESEAL"} mitsenden – und ' +
            'vorher klären, warum das bisherige Siegel nicht mehr passt.'
          : 'Versiegelt den aktuellen Stand des Altbestands. Zum Bestätigen ' +
            '{"confirm":"SEAL"} mitsenden.',
        aktuellerSiegelzustand: vorher.backlogSealStatus,
      });
      return;
    }

    const result = await auditService.sealBacklog({
      userEmail: req.user?.email,
      ipAddress: req.ip || (req.socket as any)?.remoteAddress,
    });

    // In den ALARMKANAL, nicht nur ins Audit-Log (Pentest R185-01).
    //
    // Die CRITICAL-Zeile im Audit-Log gab es schon - aber die muss jemand
    // lesen, und genau das ist die R183-02/R184-01-Klasse. Der automatische
    // Rueckhalt des Gegenbuchs haengt an `valid`, und `valid` ueberlebt ein
    // ersetzendes Siegel per Konstruktion. Der einzige maschinell erkennbare
    // Anker ist der Wechsel der Wurzel - der gehoert dorthin, wo etwas von
    // selbst passiert.
    const ctx = contextFromRequest(req);
    emitSecurityEvent({
      type: 'AUDIT_SEAL_CHANGED',
      severity: siegelSteht ? 'CRITICAL' : 'HIGH',
      message: siegelSteht
        ? `Bestandssiegel ERSETZT (${result.sealedCount} Alteinträge, id ${result.fromId}–` +
          `${result.toId}). Die bisherige Beweisgrundlage gilt nicht mehr; der Zustand ` +
          `davor war "${vorher.backlogSealStatus}". Wenn das keine geplante Maßnahme war, ` +
          'ist es ein Befund.'
        : `Bestandssiegel erstmals gesetzt (${result.sealedCount} Alteinträge, id ` +
          `${result.fromId}–${result.toId}). Ab jetzt fallen Änderungen am Altbestand auf.`,
      ipAddress: ctx.ipAddress,
      userId: req.user?.userId,
      userEmail: req.user?.email,
      endpoint: ctx.endpoint,
      details: {
        ersetzt: siegelSteht,
        zustandVorher: vorher.backlogSealStatus,
        wurzelVorher: vorher.backlogSealRoot,
        wurzelNachher: result.root,
        befundVorher: {
          manipuliert: vorher.tamperedEntries,
          luecken: vorher.chainGaps,
          altbestandVeraendert: vorher.backlogTampered,
          altbestandFehlend: vorher.backlogMissing,
        },
      },
    });

    res.json({
      success: true,
      data: result,
      message:
        `${result.sealedCount} Alteinträge (id ${result.fromId}–${result.toId}) versiegelt. ` +
        'Spätere Änderungen an diesen Einträgen fallen ab sofort auf.' +
        (siegelSteht
          ? ' ACHTUNG: Dies war ein ERSETZENDES Siegel – die vorherige Beweisgrundlage ' +
            'gilt nicht mehr.'
          : ''),
    });
  } catch (error) {
    console.error('Fehler beim Versiegeln des Altbestands:', error);
    res.status(400).json({
      success: false,
      error: error instanceof Error ? error.message : 'Fehler beim Versiegeln',
    });
  }
}

/**
 * Retention-Policies abrufen
 */
export async function getRetentionPolicies(req: AuthRequest, res: Response) {
  try {
    const policies = await auditService.getRetentionPolicies();
    res.json({ success: true, data: policies });
  } catch (error) {
    console.error('Fehler beim Abrufen der Retention-Policies:', error);
    res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Policies' });
  }
}

/**
 * Retention-Policy aktualisieren
 */
export async function updateRetentionPolicy(req: AuthRequest, res: Response) {
  try {
    const id = parseInt(req.params.id);
    if (Number.isNaN(id)) {
      return res.status(400).json({ success: false, error: 'Ungültige Policy-ID' });
    }
    const { retentionDays, description, legalBasis, isActive } = req.body;

    // Die Richtlinie bestimmt, WAS der Cleanup vernichtet – sie ist die
    // geladene Waffe, der Cleanup nur der Abzug (Pentest R183-01). Bisher war
    // ausgerechnet der Abzug gegatet und die Waffe frei zugaenglich: Ein
    // `retentionDays: 0` ging ohne Bestaetigung durch und wurde nur als MEDIUM
    // protokolliert, waehrend seine Wirkung CRITICAL ist. Die ausloesende Tat
    // war damit leiser als die Folge – und lag unter der Schwelle, bei der
    // jemand hinsieht.
    const bisher = (await auditService.getRetentionPolicies()).find((p) => p.id === id);
    if (!bisher) {
      return res.status(404).json({ success: false, error: 'Aufbewahrungsrichtlinie nicht gefunden' });
    }

    const neueTage = typeof retentionDays === 'number' ? retentionDays : bisher.retentionDays;

    // Untergrenze: Auth-Eintraege sind der Einbruchsbeleg schlechthin. Eine
    // Aufbewahrung von faktisch null macht sie mit dem naechsten Cleanup
    // spurlos entfernbar – ueber einen sanktionierten Pfad.
    const UNTERGRENZE: Record<string, number> = { Authentication: 30, AuditLog: 30 };
    const grenze = UNTERGRENZE[bisher.resourceType];
    if (grenze !== undefined && neueTage < grenze) {
      return res.status(400).json({
        success: false,
        error:
          `Für ${bisher.resourceType} sind mindestens ${grenze} Tage Aufbewahrung vorgeschrieben ` +
          `(angefragt: ${neueTage}). Diese Einträge belegen Anmeldungen und Zugriffe – eine ` +
          `Aufbewahrung nahe null macht sie beim nächsten Cleanup spurlos entfernbar.`,
      });
    }

    // Absenken ist die gefaehrliche Richtung und braucht dieselbe ausdrueckliche
    // Bestaetigung wie der Cleanup selbst.
    const senktAb = neueTage < bisher.retentionDays;
    if (senktAb && req.body?.confirm !== 'SHORTEN') {
      return res.status(400).json({
        success: false,
        error:
          `Das verkürzt die Aufbewahrung von ${bisher.retentionDays} auf ${neueTage} Tage. ` +
          'Beim nächsten Cleanup werden dadurch Einträge endgültig gelöscht, die heute noch ' +
          'da sind. Zum Bestätigen {"confirm":"SHORTEN"} mitsenden.',
      });
    }

    const policy = await auditService.updateRetentionPolicy(id, {
      retentionDays,
      description,
      legalBasis,
      isActive,
    });

    await logChange({
      req, action: 'UPDATE', resourceType: 'RetentionPolicy',
      resourceId: id.toString(),
      // Absenkung wird wie ihre Folge eingestuft: CRITICAL, nicht MEDIUM.
      sensitivity: senktAb ? 'CRITICAL' : undefined,
      label: senktAb
        ? `Aufbewahrung VERKÜRZT für ${bisher.resourceType}` +
          (bisher.sensitivity ? `/${bisher.sensitivity}` : '') +
          `: ${bisher.retentionDays} → ${neueTage} Tage`
        : `Aufbewahrungsrichtlinie aktualisiert`,
      before: { retentionDays: bisher.retentionDays },
      details: { retentionDays: neueTage },
    });

    res.json({ success: true, data: policy });
  } catch (error) {
    console.error('Fehler beim Aktualisieren der Retention-Policy:', error);
    res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' });
  }
}

/**
 * Retention-Cleanup manuell ausführen
 */
export async function runRetentionCleanup(req: AuthRequest, res: Response) {
  try {
    // Auch hier ausdrueckliche Bestaetigung: Cleanup loescht Audit-Zeilen
    // endgueltig und reisst dabei die Kette auf (Pentest R170-01).
    if (req.body?.confirm !== 'CLEANUP') {
      res.status(400).json({
        success: false,
        error:
          'Cleanup löscht Audit-Einträge endgültig. Zum Bestätigen ' +
          '{"confirm":"CLEANUP"} mitsenden.',
      });
      return;
    }

    const result = await auditService.runRetentionCleanup();

    res.json({
      success: true,
      data: result,
      message: `${result.deletedCount} alte Audit-Logs wurden gelöscht`,
    });
  } catch (error) {
    console.error('Fehler beim Retention-Cleanup:', error);
    res.status(500).json({ success: false, error: 'Fehler beim Cleanup' });
  }
}
