import { AuditAction, AuditSensitivity, Prisma } from '@prisma/client';
import crypto from 'crypto';
import { encrypt, decrypt } from '../utils/encryption.js';
import prisma from '../lib/prisma.js';

/**
 * Vereinfachte Audit-Log-Funktion für gezielte Änderungsprotokolle.
 * Wird direkt in Controllern aufgerufen mit aussagekräftigen Details.
 */
export async function logChange(opts: {
  req: any;                    // Express Request (für userId, email, IP)
  action: AuditAction;
  resourceType: string;
  resourceId?: string;
  label: string;               // Menschenlesbares Label z.B. "Vollmacht für Stefan Hacker widerrufen"
  details?: Record<string, unknown>;  // Zusätzliche Details z.B. { vorher: 'erteilt', nachher: 'widerrufen' }
  customerId?: number;
  /// Ueberschreibt die aus dem Ressourcentyp abgeleitete Stufe. Noetig, wenn
  /// eine Aktion gefaehrlicher ist als ihr Typ vermuten laesst - etwa das
  /// Verkuerzen einer Aufbewahrungsfrist, dessen Folge CRITICAL ist
  /// (Pentest R183-01).
  sensitivity?: AuditSensitivity;
  before?: Record<string, unknown>;
}) {
  try {
    const user = opts.req?.user;
    await createAuditLog({
      userId: user?.userId,
      userEmail: user?.email || 'system',
      userRole: user?.isCustomerPortal ? 'Kundenportal' : 'Mitarbeiter',
      customerId: user?.customerId,
      isCustomerPortal: user?.isCustomerPortal,
      action: opts.action,
      resourceType: opts.resourceType,
      resourceId: opts.resourceId,
      resourceLabel: opts.label,
      endpoint: opts.req?.path || '',
      httpMethod: opts.req?.method || '',
      ipAddress: opts.req?.socket?.remoteAddress || opts.req?.headers?.['x-forwarded-for'] || 'unknown',
      dataSubjectId: opts.customerId,
      sensitivity: opts.sensitivity,
      changesBefore: opts.before,
      changesAfter: opts.details,
    });
  } catch (error) {
    console.error('[logChange] Fehler:', error);
  }
}

export interface CreateAuditLogData {
  userId?: number;
  userEmail: string;
  userRole?: string;
  customerId?: number;
  isCustomerPortal?: boolean;
  action: AuditAction;
  sensitivity?: AuditSensitivity;
  resourceType: string;
  resourceId?: string;
  resourceLabel?: string;
  endpoint: string;
  httpMethod: string;
  ipAddress: string;
  userAgent?: string;
  changesBefore?: Record<string, unknown>;
  changesAfter?: Record<string, unknown>;
  dataSubjectId?: number;
  legalBasis?: string;
  success?: boolean;
  errorMessage?: string;
  durationMs?: number;
}

export interface AuditLogSearchParams {
  userId?: number;
  customerId?: number;
  dataSubjectId?: number;
  action?: AuditAction;
  sensitivity?: AuditSensitivity;
  resourceType?: string;
  resourceId?: string;
  startDate?: Date;
  endDate?: Date;
  success?: boolean;
  search?: string;
  page?: number;
  limit?: number;
}

/**
 * Generiert einen SHA-256 Hash für einen Audit-Log-Eintrag
 */
function generateHash(data: {
  userEmail: string;
  action: AuditAction;
  resourceType: string;
  resourceId?: string | null;
  endpoint: string;
  createdAt: Date;
  previousHash?: string | null;
}): string {
  // WICHTIG – Pentest R121 (Verify-Fehlalarm, ~73% "manipuliert"):
  // Beim Schreiben war `resourceId` bei middleware-generierten Einträgen
  // oft `undefined`. JSON.stringify LÄSST einen undefined-Wert weg – der
  // Hash wurde also OHNE `resourceId`-Key gebildet. In der DB landet der
  // Wert aber als `NULL`. verifyIntegrity/rehashAll lasen ihn als `null`
  // zurück, und `JSON.stringify({resourceId: null})` schreibt
  // `"resourceId":null` REIN → anderer Hash → falscher Tamper-Alarm für
  // jede Zeile mit leerem resourceId.
  //
  // Fix: nullish resourceId weglassen – reproduziert exakt das historische
  // Schreibverhalten (undefined → Key weg). Kein Caller hat je `null`
  // explizit übergeben (verifiziert), daher matchen ALLE Bestands-Hashes
  // ohne Rehash. Feldreihenfolge bleibt identisch zur Alt-Serialisierung.
  const payload: Record<string, unknown> = {
    userEmail: data.userEmail,
    action: data.action,
    resourceType: data.resourceType,
  };
  if (data.resourceId !== null && data.resourceId !== undefined) {
    payload.resourceId = data.resourceId;
  }
  payload.endpoint = data.endpoint;
  payload.createdAt = data.createdAt.toISOString();
  payload.previousHash = data.previousHash || '';

  return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
}

/**
 * Historische Hash-Variante (Bestandsdaten ~08.02.–01.05.2026).
 *
 * Der R121-Fix ging davon aus, dass `resourceId` beim Schreiben immer
 * `undefined` war (→ Key faellt bei JSON.stringify weg) und daher ALLE
 * Bestands-Hashes ohne Rehash matchen. Das stimmt erst ab ~01.05.2026:
 * aeltere Zeilen wurden mit explizitem `null` serialisiert, der Key war also
 * DRIN. Ergebnis war ein Dauer-Fehlalarm ueber ~3100 Zeilen (67 % des Logs) –
 * und ein Alarm, der staendig grundlos ausloest, verdeckt echte Manipulation.
 *
 * Diese Funktion reproduziert exakt das alte Schreibverhalten, damit
 * verifyIntegrity Altbestand als gueltig erkennt – OHNE Rehash. Ein Rehash
 * waere der naheliegende Schnellfix, wuerde die Manipulations-Beweiskraft der
 * Vergangenheit aber unwiederbringlich zerstoeren.
 *
 * Sicherheit: Beide Varianten hashen dieselben Feldwerte, nur die
 * Serialisierung des nullish `resourceId` unterscheidet sich. Ein Angreifer
 * gewinnt dadurch keinen Spielraum, Inhalte zu aendern – nur die Kodierung
 * eines leeren Feldes ist doppelt zulaessig.
 */
function generateHashLegacy(data: {
  userEmail: string;
  action: AuditAction;
  resourceType: string;
  resourceId?: string | null;
  endpoint: string;
  createdAt: Date;
  previousHash?: string | null;
}): string {
  const payload: Record<string, unknown> = {
    userEmail: data.userEmail,
    action: data.action,
    resourceType: data.resourceType,
    resourceId: data.resourceId ?? null,
    endpoint: data.endpoint,
    createdAt: data.createdAt.toISOString(),
    previousHash: data.previousHash || '',
  };

  return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
}

/**
 * Hash-Version 2 (Pentest R166-02): deckt ALLE Inhaltsspalten ab.
 *
 * Version 1 hashte nur 7 Felder (userEmail, action, resourceType, resourceId,
 * endpoint, createdAt, previousHash). Nicht abgedeckt waren u. a.
 * `changesBefore`/`changesAfter` – also die eigentliche Nutzlast –, dazu
 * `success`, `ipAddress`, `resourceLabel`, `dataSubjectId`, `userId`,
 * `customerId`. Ein nachtraeglicher Einzeledit an genau diesen Feldern
 * (z. B. `success` false→true oder das Umschreiben von `changesAfter`) war
 * damit unsichtbar – exakt der Angriff, gegen den die Kette schuetzen soll.
 *
 * Bestandszeilen behalten `hashVersion = 1` und werden weiter mit dem alten
 * Verfahren geprueft; ein Rehash waere nicht noetig und wuerde die
 * Beweiskraft der Vergangenheit zerstoeren.
 *
 * `undefined` wird bewusst zu `null` normalisiert, damit die Serialisierung
 * deterministisch ist (die Uneindeutigkeit an dieser Stelle war die Ursache
 * des Dauer-Fehlalarms bei Version 1).
 */
export interface AuditHashV2Input {
  userId?: number | null;
  userEmail: string;
  userRole?: string | null;
  customerId?: number | null;
  isCustomerPortal?: boolean | null;
  action: AuditAction;
  sensitivity?: AuditSensitivity | null;
  resourceType: string;
  resourceId?: string | null;
  resourceLabel?: string | null;
  endpoint: string;
  httpMethod?: string | null;
  ipAddress?: string | null;
  userAgent?: string | null;
  changesBefore?: string | null;
  changesAfter?: string | null;
  changesEncrypted?: boolean | null;
  dataSubjectId?: number | null;
  legalBasis?: string | null;
  success?: boolean | null;
  errorMessage?: string | null;
  durationMs?: number | null;
  createdAt: Date;
  previousHash?: string | null;
}

/**
 * Externer Anker (Pentest R166/R167, Design-Empfehlung).
 *
 * Bis Version 2 war die Kette selbsttragend: Wer die DB schreiben kann, kann
 * jede Zeile aendern und saemtliche Folgehashes konsistent nachziehen – die
 * Pruefung meldet dann sauber "gueltig". Version 3 signiert denselben Inhalt
 * stattdessen per HMAC-SHA256 mit einem Schluessel, der NICHT in der Datenbank
 * liegt (`AUDIT_HMAC_KEY`). Ohne diesen Schluessel laesst sich keine gueltige
 * Signatur erzeugen – reiner DB-Schreibzugriff genuegt damit nicht mehr.
 *
 * Fail-safe: Ohne konfigurierten Schluessel wird weiterhin Version 2
 * geschrieben. Audit-Logging darf nie an fehlender Konfiguration scheitern.
 * `AUDIT_HMAC_KEY_OLD` erlaubt einen Schluesselwechsel ohne Rehash – bei der
 * Pruefung wird zusaetzlich gegen den alten Schluessel getestet.
 */
/**
 * Ein nicht ersetzter Platzhalter aus `.env.example` (z. B.
 * `<hier-eigenen-wert-eintragen>`) darf NICHT als Schluessel gelten. Sonst
 * wuerde mit einem oeffentlich im Repository stehenden Wert gesiegelt – das
 * waere schlechter als gar kein Siegel, weil es Sicherheit vortaeuscht.
 */
function istPlatzhalter(k: string): boolean {
  return k.includes('<') || k.includes('>') || /^(change|dein|your|hier)/i.test(k);
}

let platzhalterGemeldet = false;

function auditHmacKey(): string | null {
  const k = process.env.AUDIT_HMAC_KEY?.trim();
  if (!k) return null;
  if (istPlatzhalter(k)) {
    if (!platzhalterGemeldet) {
      platzhalterGemeldet = true;
      console.warn(
        '[Audit] AUDIT_HMAC_KEY enthält noch den Platzhalter aus .env.example – ' +
        'das Audit-Siegel bleibt deaktiviert. Eigenen Wert erzeugen: openssl rand -hex 32',
      );
    }
    return null;
  }
  return k;
}

/**
 * Frueher verwendete Schluessel. Kommagetrennt, damit auch ein ZWEITER Wechsel
 * moeglich ist, ohne die zuerst signierten Eintraege zu verlieren - mit nur
 * einem Platz wuerden die aeltesten sonst als "manipuliert" gemeldet.
 */
function auditHmacKeysOld(): string[] {
  return (process.env.AUDIT_HMAC_KEY_OLD || '')
    .split(',')
    .map((k) => k.trim())
    .filter((k) => k.length > 0 && !istPlatzhalter(k));
}

function generateHashV3(data: AuditHashV2Input, key: string): string {
  return crypto.createHmac('sha256', key)
    .update(JSON.stringify(buildContentPayload(data, 3)))
    .digest('hex');
}

function generateHashV2(data: AuditHashV2Input): string {
  return crypto.createHash('sha256')
    .update(JSON.stringify(buildContentPayload(data, 2)))
    .digest('hex');
}

/** Gemeinsamer Inhalts-Payload fuer Version 2 (SHA-256) und 3 (HMAC). */
function buildContentPayload(data: AuditHashV2Input, version: 2 | 3): Record<string, unknown> {
  const n = <T>(v: T | null | undefined): T | null => (v === undefined ? null : v);
  // Feldreihenfolge ist Teil des Hashes und darf nicht veraendert werden.
  return {
    v: version,
    userId: n(data.userId),
    userEmail: data.userEmail,
    userRole: n(data.userRole),
    customerId: n(data.customerId),
    isCustomerPortal: n(data.isCustomerPortal) ?? false,
    action: data.action,
    sensitivity: n(data.sensitivity),
    resourceType: data.resourceType,
    resourceId: n(data.resourceId),
    resourceLabel: n(data.resourceLabel),
    endpoint: data.endpoint,
    httpMethod: n(data.httpMethod),
    ipAddress: n(data.ipAddress),
    userAgent: n(data.userAgent),
    changesBefore: n(data.changesBefore),
    changesAfter: n(data.changesAfter),
    changesEncrypted: n(data.changesEncrypted) ?? false,
    dataSubjectId: n(data.dataSubjectId),
    legalBasis: n(data.legalBasis),
    success: n(data.success) ?? true,
    errorMessage: n(data.errorMessage),
    durationMs: n(data.durationMs),
    createdAt: data.createdAt.toISOString(),
    previousHash: data.previousHash || '',
  };
}

/**
 * Bestimmt die Sensitivität basierend auf dem Ressourcentyp
 */
function determineSensitivity(resourceType: string): AuditSensitivity {
  const sensitivityMap: Record<string, AuditSensitivity> = {
    // CRITICAL
    Authentication: 'CRITICAL',
    BankCard: 'CRITICAL',
    IdentityDocument: 'CRITICAL',
    // Klartext-Passwort-Reads – jeder Decrypt-Vorgang muss nachvollziehbar sein
    PortalPassword: 'CRITICAL',
    ContractPassword: 'CRITICAL',
    SimCardCredentials: 'CRITICAL',
    InternetCredentials: 'CRITICAL',
    SipCredentials: 'CRITICAL',
    MailboxCredentials: 'CRITICAL',
    // HIGH
    Customer: 'HIGH',
    User: 'HIGH',
    CustomerConsent: 'HIGH',
    DataDeletionRequest: 'HIGH',
    // Abgewehrter IDOR-/Cross-Boundary-Zugriffsversuch (canAccess*-403).
    // HIGH, weil ein Treffer auf gezielte Fremddaten-Enumeration hindeutet.
    AccessDenied: 'HIGH',
    // MEDIUM
    Contract: 'MEDIUM',
    Address: 'MEDIUM',
    Meter: 'MEDIUM',
    MeterReading: 'MEDIUM',
    StressfreiEmail: 'MEDIUM',
    CachedEmail: 'MEDIUM',
    // LOW
    Provider: 'LOW',
    Tariff: 'LOW',
    SalesPlatform: 'LOW',
    AppSetting: 'LOW',
    ContractCategory: 'LOW',
    CancellationPeriod: 'LOW',
    ContractDuration: 'LOW',
  };
  return sensitivityMap[resourceType] || 'MEDIUM';
}

/**
 * Prüft ob Änderungen verschlüsselt werden sollen.
 * Deaktiviert - sensible Felder werden bereits von der Prisma-Middleware als [REDACTED] gefiltert.
 */
function shouldEncryptChanges(_resourceType: string): boolean {
  return false;
}

/**
 * Erstellt einen neuen Audit-Log-Eintrag mit Hash-Kette
 */
export async function createAuditLog(data: CreateAuditLogData): Promise<void> {
  try {
    // Sensitivität bestimmen falls nicht angegeben
    const sensitivity = data.sensitivity || determineSensitivity(data.resourceType);

    // Änderungen serialisieren und ggf. verschlüsseln
    let changesBefore: string | null = null;
    let changesAfter: string | null = null;
    let changesEncrypted = false;

    if (data.changesBefore || data.changesAfter) {
      changesEncrypted = shouldEncryptChanges(data.resourceType);

      if (data.changesBefore) {
        const json = JSON.stringify(data.changesBefore);
        changesBefore = changesEncrypted ? encrypt(json) : json;
      }

      if (data.changesAfter) {
        const json = JSON.stringify(data.changesAfter);
        changesAfter = changesEncrypted ? encrypt(json) : json;
      }
    }

    // Kette atomar fortschreiben: Vorgaenger-Hash lesen UND neuen Eintrag
    // schreiben muessen eine Einheit sein. Vorher lagen beide Schritte offen
    // nebeneinander – zwei parallele Requests lasen denselben letzten Hash und
    // haengten sich beide daran, was die Kette zerriss (echte Bruchstellen im
    // Bestand, u. a. 05.05./07.05.2026).
    //
    // Serialisiert wird ueber eine Zeilensperre auf dem Einzeiler-Mutex
    // `AuditChainLock`. Zwei Alternativen wurden verworfen:
    //  - `SELECT … FOR UPDATE` am Kettenende von AuditLog nimmt Gap-/Next-Key-
    //    Locks, die mit den gleichzeitigen INSERTs kollidieren – gemessen gingen
    //    38 von 40 parallelen Eintraegen durch Deadlocks verloren. Ein FEHLENDER
    //    Audit-Eintrag ist unsichtbar und damit schlimmer als ein sichtbarer
    //    Kettenbruch.
    //  - GET_LOCK muss auf derselben Verbindung freigegeben werden, das Release
    //    fiel damit VOR den COMMIT. In diesem Fenster las der naechste Schreiber
    //    ein noch nicht sichtbares Kettenende und hing sich an denselben
    //    Vorgaenger (Fork, Pentest R166-01).
    // Die Zeilensperre faellt erst beim COMMIT und liegt in der DB – wirkt also
    // auch ueber mehrere App-Instanzen hinweg.
    // Alles Rechenintensive (Serialisieren/Verschluesseln) passiert bewusst
    // VOR der Transaktion, damit die Sperre so kurz wie moeglich gehalten wird.
    await prisma.$transaction(async (tx) => {
      // Exklusive Zeilensperre auf den Einzeiler-Mutex. Sie faellt erst beim
      // COMMIT – dadurch sieht der naechste Schreiber die Vorgaengerzeile
      // garantiert bereits festgeschrieben (Pentest R166-01).
      await tx.$queryRaw`SELECT id FROM AuditChainLock WHERE id = 1 FOR UPDATE`;

      const lastRows = await tx.$queryRaw<Array<{ hash: string }>>`
        SELECT hash FROM AuditLog ORDER BY id DESC LIMIT 1
      `;
      const previousHash = lastRows[0]?.hash || null;
      const createdAt = new Date();

      // Version 3 (HMAC-signiert) sobald ein Schluessel konfiguriert ist,
      // sonst Version 2 – Audit-Logging darf an fehlender Konfiguration nicht
      // scheitern (fail-safe, siehe generateHashV3).
      const hmacKey = auditHmacKey();
      const inhalt = {
        userId: data.userId,
        userEmail: data.userEmail,
        userRole: data.userRole,
        customerId: data.customerId,
        isCustomerPortal: data.isCustomerPortal || false,
        action: data.action,
        sensitivity,
        resourceType: data.resourceType,
        resourceId: data.resourceId,
        resourceLabel: data.resourceLabel,
        endpoint: data.endpoint,
        httpMethod: data.httpMethod,
        ipAddress: data.ipAddress,
        userAgent: data.userAgent,
        changesBefore,
        changesAfter,
        changesEncrypted,
        dataSubjectId: data.dataSubjectId,
        legalBasis: data.legalBasis,
        success: data.success ?? true,
        errorMessage: data.errorMessage,
        durationMs: data.durationMs,
        createdAt,
        previousHash,
      };
      const hash = hmacKey ? generateHashV3(inhalt, hmacKey) : generateHashV2(inhalt);

      await tx.auditLog.create({
        data: {
          userId: data.userId,
          userEmail: data.userEmail,
          userRole: data.userRole,
          customerId: data.customerId,
          isCustomerPortal: data.isCustomerPortal || false,
          action: data.action,
          sensitivity,
          resourceType: data.resourceType,
          resourceId: data.resourceId,
          resourceLabel: data.resourceLabel,
          endpoint: data.endpoint,
          httpMethod: data.httpMethod,
          ipAddress: data.ipAddress,
          userAgent: data.userAgent,
          changesBefore,
          changesAfter,
          changesEncrypted,
          dataSubjectId: data.dataSubjectId,
          legalBasis: data.legalBasis,
          success: data.success ?? true,
          errorMessage: data.errorMessage,
          durationMs: data.durationMs,
          createdAt,
          hash,
          previousHash,
          hashVersion: hmacKey ? 3 : 2,
        },
      });
    }, {
      // READ COMMITTED: der Lesevorgang nach der Sperre muss den gerade
      // festgeschriebenen Stand sehen. Unter REPEATABLE READ koennte ein
      // Snapshot greifen, der die Vorgaengerzeile noch nicht enthaelt.
      isolationLevel: 'ReadCommitted',
      timeout: 20000,
      maxWait: 15000,
    });
  } catch (error) {
    // Audit-Logging darf niemals die Hauptoperation blockieren
    console.error('[AuditService] Fehler beim Erstellen des Audit-Logs:', error);
  }
}

/**
 * Sucht Audit-Logs mit Filtern und Paginierung
 */
export async function searchAuditLogs(params: AuditLogSearchParams) {
  const {
    userId,
    customerId,
    dataSubjectId,
    action,
    sensitivity,
    resourceType,
    resourceId,
    startDate,
    endDate,
    success,
    search,
    page = 1,
    limit = 50,
  } = params;

  const where: Prisma.AuditLogWhereInput = {};

  if (userId !== undefined) where.userId = userId;
  if (customerId !== undefined) where.customerId = customerId;
  if (dataSubjectId !== undefined) where.dataSubjectId = dataSubjectId;
  if (action) where.action = action;
  if (sensitivity) where.sensitivity = sensitivity;
  if (resourceType) where.resourceType = resourceType;
  if (resourceId) where.resourceId = resourceId;
  if (success !== undefined) where.success = success;

  if (startDate || endDate) {
    where.createdAt = {};
    if (startDate) where.createdAt.gte = startDate;
    if (endDate) where.createdAt.lte = endDate;
  }

  if (search) {
    where.OR = [
      { userEmail: { contains: search } },
      { resourceLabel: { contains: search } },
      { endpoint: { contains: search } },
    ];
  }

  const [logs, total] = await Promise.all([
    prisma.auditLog.findMany({
      where,
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit,
    }),
    prisma.auditLog.count({ where }),
  ]);

  // Entschlüsselung der Änderungen wenn nötig
  const decryptedLogs = logs.map((log) => ({
    ...log,
    changesBefore: log.changesBefore && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesBefore))
      : log.changesBefore ? JSON.parse(log.changesBefore) : null,
    changesAfter: log.changesAfter && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesAfter))
      : log.changesAfter ? JSON.parse(log.changesAfter) : null,
  }));

  return {
    data: decryptedLogs,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
    },
  };
}

/**
 * Holt einen einzelnen Audit-Log-Eintrag
 */
export async function getAuditLogById(id: number) {
  const log = await prisma.auditLog.findUnique({
    where: { id },
  });

  if (!log) return null;

  return {
    ...log,
    changesBefore: log.changesBefore && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesBefore))
      : log.changesBefore ? JSON.parse(log.changesBefore) : null,
    changesAfter: log.changesAfter && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesAfter))
      : log.changesAfter ? JSON.parse(log.changesAfter) : null,
  };
}

/**
 * Holt alle Audit-Logs für eine betroffene Person (DSGVO)
 */
export async function getAuditLogsByDataSubject(customerId: number) {
  const logs = await prisma.auditLog.findMany({
    where: { dataSubjectId: customerId },
    orderBy: { createdAt: 'desc' },
  });

  return logs.map((log) => ({
    ...log,
    changesBefore: log.changesBefore && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesBefore))
      : log.changesBefore ? JSON.parse(log.changesBefore) : null,
    changesAfter: log.changesAfter && log.changesEncrypted
      ? JSON.parse(decrypt(log.changesAfter))
      : log.changesAfter ? JSON.parse(log.changesAfter) : null,
  }));
}

/**
 * Verifiziert die Integrität der Hash-Kette
 */
/**
 * Blattwert einer Altbestands-Zeile fuer das Bestandssiegel (Pentest R171-02).
 *
 * Bewusst ueber den VOLLEN Zeileninhalt inkl. `id` und `hashVersion` – nicht
 * ueber den gespeicherten 7-Feld-Hash der Version 1. Sonst lebte die Luecke im
 * Siegel weiter: genau die 17 ungehashten Spalten waeren wieder frei
 * editierbar. `hashVersion` gehoert mit hinein, damit ein Hochstufen der Zeile
 * (V1→V2, ohne Schluessel nachrechenbar) das Blatt bricht, statt sie klammheimlich
 * aus dem Siegelbereich zu schieben.
 */
function backlogLeaf(row: Record<string, unknown>): string {
  const feld = (v: unknown) =>
    v instanceof Date ? v.toISOString() : v === undefined ? null : v;
  // Feste Reihenfolge – Teil des Blattwerts.
  const payload = [
    'id', 'userId', 'userEmail', 'userRole', 'customerId', 'isCustomerPortal',
    'action', 'sensitivity', 'resourceType', 'resourceId', 'resourceLabel',
    'endpoint', 'httpMethod', 'ipAddress', 'userAgent', 'changesBefore',
    'changesAfter', 'changesEncrypted', 'dataSubjectId', 'legalBasis', 'success',
    'errorMessage', 'durationMs', 'createdAt', 'hash', 'previousHash',
    'hashVersion',
  ].map((k) => [k, feld(row[k])]);

  return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
}

/** Wurzel ueber die Blaetter in id-Reihenfolge. */
function backlogRoot(leaves: Array<{ auditLogId: number; leafHash: string }>): string {
  const sortiert = [...leaves].sort((a, b) => a.auditLogId - b.auditLogId);
  return crypto
    .createHash('sha256')
    .update(JSON.stringify(sortiert.map((l) => [l.auditLogId, l.leafHash])))
    .digest('hex');
}

/** Endpoint-Kennung des Siegel-Markers. */
const BACKLOG_SEAL_ENDPOINT = '/api/audit-logs/seal-backlog';

/**
 * Eigener Ressourcentyp fuer den Siegel-Marker (Pentest R173-01).
 *
 * Der Marker wurde urspruenglich als `AuditLog` / `CREATE` /
 * `endpoint=/api/audit-logs/seal-backlog` gesucht. Genau diese Signatur
 * erzeugt aber die generische Audit-Middleware, wenn jemand den Endpunkt
 * aufruft – als Protokollzeile des Requests selbst, mit hoeherer id und ohne
 * `changesAfter`. Der Selektor griff dann diese Zeile statt des echten
 * Markers, `root` war `undefined`, und die Pruefung meldete dauerhaft
 * „gebrochen“. Ein Ressourcentyp, den die Middleware nie vergibt (sie leitet
 * ihn aus dem Pfad ab → `AuditLog`), macht den Marker eindeutig.
 */
const BACKLOG_SEAL_RESOURCE = 'AuditBacklogSeal';

/**
 * Versiegelt den Altbestand einmalig.
 *
 * Der Bereich ist FIX auf `[1 … v3FromId-1]` gebunden, nicht auf eine
 * Live-Abfrage `hashVersion < 3`. Sonst koennte ein Angreifer die Grenzzeile
 * hochstufen und sie damit aus der geprueften Menge draengen – sie erschiene
 * dann nur als harmlose Luecke. Zeilen ab `v3FromId` deckt die Versionsgrenze
 * ab (sie MUESSEN V3 sein), es bleibt also keine Naht dazwischen.
 */
/**
 * Kontrollwert fuer die externe Notarisierung (Gegenbuch).
 *
 * Alle bisherigen Anker leben IM System: Hash, Versionsgrenze, Manifest,
 * Bestandssiegel, Siegel-Marker. Wer Schreibzugriff auf die Datenbank hat,
 * sitzt am Ende immer schon auf der Ebene, die beweisen soll – zuletzt gezeigt
 * in R174-01. Ein Gegenbuch an einem fremden Ort durchbricht das.
 *
 * Der Wert enthaelt bewusst `maxId`: Ein blosser Kopf-Hash erkennt zwar
 * Umschreiben, aber kein ABSCHNEIDEN am Ende. Mit ankertem `maxId` ist
 * „juengster beglaubigter maxId > aktueller maxId“ der Beweis, dass Zeilen
 * verschwunden sind.
 *
 * `atId` erlaubt der Gegenstelle, einen FRUEHER beglaubigten Kopf erneut
 * abzufragen und zu reproduzieren – ohne das koennte sie nur den aktuellen
 * Stand sehen und nichts nachrechnen.
 *
 * Enthaelt keine Geheimnisse: nur Hashes und Zaehlstaende.
 */
export async function getCheckpoint(atId?: number): Promise<{
  ts: string;
  maxId: number | null;
  chainHead: string | null;
  sealRoot: string | null;
  sealLeafCount: number;
  atId?: number;
  atHash?: string | null;
}> {
  const kopf = await prisma.auditLog.findFirst({
    orderBy: { id: 'desc' },
    select: { id: true, hash: true },
  });

  // BEWUSST KEIN verifyIntegrity() hier (Pentest R175-02): das laeuft ueber
  // alle Zeilen und kostet bei grossen Logs Sekunden je Aufruf – ein
  // authentifizierter Leser koennte damit die Datenbank in die Knie zwingen.
  // Gebraucht wird von der Vollpruefung ohnehin nur die Siegel-Wurzel, und die
  // steht im juengsten gueltigen Marker. Der Kopf-Hash kommt direkt aus der
  // Kopfzeile.
  const c3 = await prisma.auditLog.aggregate({
    where: { hashVersion: { gte: 3 } },
    _min: { id: true },
  });
  const v3FromId = c3._min.id;
  const schluessel = [auditHmacKey(), ...auditHmacKeysOld()].filter(
    (k): k is string => !!k,
  );
  const markerKandidaten = await prisma.auditLog.findMany({
    where: { resourceType: BACKLOG_SEAL_RESOURCE, endpoint: BACKLOG_SEAL_ENDPOINT },
    orderBy: { id: 'desc' },
    take: 20,
  });
  let sealRoot: string | null = null;
  if (schluessel.length && v3FromId !== null) {
    for (const r of markerKandidaten) {
      if (r.id < v3FromId || r.hashVersion < 3) continue;
      if (!schluessel.some((k) => r.hash === generateHashV3(r as unknown as AuditHashV2Input, k))) continue;
      try {
        const m = JSON.parse(r.changesAfter || '{}');
        if (typeof m.root === 'string' && m.root.length > 0) {
          sealRoot = m.root;
          break;
        }
      } catch {
        /* naechster Kandidat */
      }
    }
  }
  const blattAnzahl = await prisma.auditBacklogSeal.count();

  const ergebnis: {
    ts: string;
    maxId: number | null;
    chainHead: string | null;
    sealRoot: string | null;
    sealLeafCount: number;
    atId?: number;
    atHash?: string | null;
  } = {
    ts: new Date().toISOString(),
    maxId: kopf?.id ?? null,
    chainHead: kopf?.hash ?? null,
    sealRoot,
    // Reicht der Gegenstelle, um das Verschwinden des Siegels zu bemerken:
    // Blaetter ohne Wurzel = Marker entfernt.
    sealLeafCount: blattAnzahl,
  };

  if (atId !== undefined && Number.isFinite(atId)) {
    const zeile = await prisma.auditLog.findUnique({
      where: { id: atId },
      select: { hash: true },
    });
    ergebnis.atId = atId;
    // null bedeutet: die beglaubigte Zeile existiert nicht mehr.
    ergebnis.atHash = zeile?.hash ?? null;
  }

  return ergebnis;
}

export async function sealBacklog(
  ausgeloestVon?: { userEmail?: string; ipAddress?: string },
): Promise<{ sealedCount: number; fromId: number; toId: number; root: string }> {
  if (!auditHmacKey()) {
    throw new Error(
      'Bestandssiegel erfordert einen konfigurierten AUDIT_HMAC_KEY – ohne ihn liesse sich die Wurzel nachrechnen.',
    );
  }
  const c3 = await prisma.auditLog.aggregate({
    where: { hashVersion: { gte: 3 } },
    _min: { id: true },
  });
  const v3FromId = c3._min.id;
  if (v3FromId === null) {
    throw new Error(
      'Noch keine signierten Einträge vorhanden – bitte zuerst AUDIT_HMAC_KEY setzen und das Backend neu starten.',
    );
  }
  const toId = v3FromId - 1;
  if (toId < 1) {
    throw new Error('Kein Altbestand vorhanden – es gibt nichts zu versiegeln.');
  }

  // Zustand VOR dem Siegeln festhalten (R173-03).
  const vorher = await verifyIntegrity();
  const vorbefund = {
    vorherigesSiegel:
      vorher.backlogSealStatus === 'kein_siegel'
        ? null
        : { status: vorher.backlogSealStatus, root: vorher.backlogSealRoot },
    befund: {
      manipuliert: vorher.tamperedEntries,
      altbestand_veraendert: vorher.backlogTampered,
      altbestand_fehlend: vorher.backlogMissing,
      ketten_luecken: vorher.chainGaps,
    },
  };

  const rows = await prisma.auditLog.findMany({
    where: { id: { lte: toId } },
    orderBy: { id: 'asc' },
  });
  const leaves = rows.map((r) => ({
    auditLogId: r.id,
    leafHash: backlogLeaf(r as unknown as Record<string, unknown>),
  }));
  const root = backlogRoot(leaves);

  await prisma.$transaction(async (tx) => {
    await tx.auditBacklogSeal.deleteMany({});
    for (let i = 0; i < leaves.length; i += 500) {
      await tx.auditBacklogSeal.createMany({ data: leaves.slice(i, i + 500) });
    }
  }, { timeout: 60000, maxWait: 20000 });

  // Marker ist selbst HMAC-signiert (Version 3) – damit ist die Wurzel
  // signiert, ohne dass es einen zweiten Signaturpfad braucht.
  //
  // Vorzustand mitschreiben (Pentest R173-03): Ein erneutes Siegeln wuerde
  // sonst eine zwischenzeitliche Manipulation absorbieren und die gefaelschte
  // Zeile als echt zertifizieren – mit noch weniger Spur als ein Rehash. Der
  // Marker haelt deshalb fest, was VOR dem Siegeln galt: der Befund und die
  // Wurzel des vorherigen Siegels.
  await createAuditLog({
    userEmail: ausgeloestVon?.userEmail || 'system',
    userRole: 'System',
    action: 'CREATE',
    resourceType: BACKLOG_SEAL_RESOURCE,
    resourceLabel:
      `Bestandssiegel erstellt: ${leaves.length} Alteinträge (id 1–${toId})` +
      (vorbefund.vorherigesSiegel
        ? ' – ERNEUTES Siegeln, vorheriges Siegel wird ersetzt'
        : ''),
    endpoint: BACKLOG_SEAL_ENDPOINT,
    httpMethod: 'POST',
    ipAddress: ausgeloestVon?.ipAddress || 'system',
    sensitivity: 'CRITICAL',
    changesBefore: vorbefund,
    changesAfter: { fromId: 1, toId, count: leaves.length, root },
    success: true,
  });

  return { sealedCount: leaves.length, fromId: 1, toId, root };
}

export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
  valid: boolean;
  checkedCount: number;
  /**
   * Alle noch offenen Beanstandungen: veraenderte Zeilen plus Luecken, die
   * NICHT vom Bestandssiegel beglaubigt sind. Nur an dieser Liste haengt
   * `valid` – beglaubigte Alt-Luecken stehen in `attestedGaps`.
   */
  invalidEntries: number[];
  /**
   * ERNST: Der Inhalt der Zeile passt nicht mehr zu ihrem Hash – jemand hat
   * einen bestehenden Eintrag nachtraeglich veraendert.
   */
  tamperedEntries: number[];
  /**
   * MEIST HARMLOS: Der Inhalt stimmt, aber die Verkettung zur Vorgaengerzeile
   * passt nicht. Ursachen: parallel geschriebene Eintraege (bis zum Fix der
   * Race-Condition) oder geloeschte Zeilen (Retention-Cleanup). Kein Hinweis
   * auf Manipulation – die Zeilen selbst sind unveraendert.
   */
  chainGaps: number[];
  /**
   * Teilmenge von `chainGaps`, die NICHT durch ein protokolliertes
   * Loeschungs-Manifest erklaert ist. Nur diese sind erklaerungsbeduerftig.
   */
  unexplainedGaps: number[];
  /**
   * HMAC-signierte Zeilen, die mangels konfiguriertem `AUDIT_HMAC_KEY` nicht
   * geprueft werden konnten. Bewusst KEIN Manipulationsverdacht – sonst waere
   * eine fehlende Konfiguration ein Fehlalarm ueber das gesamte Log.
   */
  unverifiableEntries: number[];
  /**
   * Teilmenge von `chainGaps`, die beim Versiegeln des Altbestands bereits
   * bestand und im signierten Siegel-Marker als Vorbefund festgehalten ist.
   * Diese Luecken sind BEGLAUBIGT: sie bleiben sichtbar, kippen `valid` aber
   * nicht mehr (siehe ausfuehrliche Begruendung an der Berechnung unten).
   */
  attestedGaps: number[];
  /**
   * Protokollierte Neuberechnungen der Kette (Rehash), aelteste zuerst.
   *
   * Ein Rehash verknuepft alle Eintraege neu. Danach ist die Kette
   * zwangslaeufig stimmig - auch ueber Loeschungen hinweg, die vorher als
   * Luecken sichtbar waren. „Lueckenlos verkettet“ heisst nach einem Rehash
   * also nur noch: seit dem Rehash. Wer das nicht mitliest, nimmt eine
   * Entwarnung mit, die es nicht gibt.
   */
  rehashes: Array<{
    id: number;
    zeitpunkt: string;
    neuBerechnet: number | null;
    /** Marker mit gueltiger HMAC-Signatur? Siehe Kommentar an der Auswertung. */
    signiert: boolean;
    /** Befund unmittelbar VOR dem Rehash - was also uebertuencht wurde. */
    vorbefund: { manipuliert: number; luecken: number } | null;
  }>;
  /**
   * Zustand des Bestandssiegels ueber den nicht signierbaren Altbestand.
   * `kein_siegel` = nie erstellt. `entfernt` = Blaetter vorhanden, aber kein
   * gueltiger Marker mehr – der Anker wurde herausgeloest (Pentest R174-01).
   */
  backlogSealStatus: 'kein_siegel' | 'intakt' | 'leer' | 'gebrochen' | 'entfernt' | 'nicht_noetig';
  /** Altbestands-Zeilen, deren Inhalt vom Siegel abweicht. */
  backlogTampered: number[];
  /** Gesiegelte Zeilen, die nicht mehr existieren – Beweismaterial entfernt. */
  backlogMissing: number[];
  /** Anzahl gueltiger Siegel-Marker; >1 bedeutet erneutes Siegeln. */
  backlogSealCount: number;
  /** Wurzel des aktuell gueltigen Siegels. */
  backlogSealRoot: string | null;
}> {
  const where: Prisma.AuditLogWhereInput = {};

  if (fromId !== undefined) where.id = { gte: fromId };
  if (toId !== undefined) where.id = { ...(where.id as object || {}), lte: toId };

  const logs = await prisma.auditLog.findMany({
    where,
    orderBy: { id: 'asc' },
    // Version 2 hasht alle Inhaltsspalten – daher vollstaendig laden.
    select: {
      id: true,
      userId: true,
      userEmail: true,
      userRole: true,
      customerId: true,
      isCustomerPortal: true,
      action: true,
      sensitivity: true,
      resourceType: true,
      resourceId: true,
      resourceLabel: true,
      endpoint: true,
      httpMethod: true,
      ipAddress: true,
      userAgent: true,
      changesBefore: true,
      changesAfter: true,
      changesEncrypted: true,
      dataSubjectId: true,
      legalBasis: true,
      success: true,
      errorMessage: true,
      durationMs: true,
      createdAt: true,
      hash: true,
      previousHash: true,
      hashVersion: true,
    },
  });

  // ---------------------------------------------------------------------
  // Version-Floor (Pentest R167-01)
  //
  // Vorher bestimmte die Zeile SELBST ueber `hashVersion`, wie streng sie
  // geprueft wird – und `hashVersion` ist nicht gehasht. Ein Angreifer mit
  // Schreibzugriff konnte also 2→1 zuruecksetzen, die Felder aendern, die nur
  // V2 abdeckt (z. B. `success` false→true), und den schwachen V1-Hash ueber
  // die 7 unveraenderten Felder nachziehen: Die Pruefung meldete "gueltig".
  //
  // Jetzt leitet sich die ERWARTETE Version aus der Kette ab: ab der ersten
  // jemals mit V2 geschriebenen Zeile muss jede weitere Zeile V2 sein. Die
  // Grenze laesst sich durch das Herabstufen einer einzelnen Zeile nicht
  // verschieben (das Minimum bleibt), ein Angreifer muesste saemtliche
  // V2-Zeilen ab der Grenze herabstufen und die komplette Kette neu rechnen –
  // das entspricht einem vollstaendigen Rehash und damit dem bekannten
  // Grenzfall "vollstaendig kompromittierter DB-Zugang".
  //
  // Betriebshinweis: Bei einem rollierenden Deploy, bei dem kurzzeitig alte und
  // neue Instanz parallel schreiben, koennen echte V1-Zeilen nach der Grenze
  // entstehen und werden dann angezeigt. Beim hier ueblichen Deploy
  // (pull + rebuild + restart einer Instanz) tritt das nicht auf.
  // Grenze pro Stufe: ab der ersten je mit Version >= v geschriebenen Zeile
  // muss jede weitere Zeile mindestens Version v haben. Verallgemeinert auf
  // Version 3 (HMAC-Anker), damit auch ein Ruecksprung 3→2 auffliegt.
  const [c2, c3] = await Promise.all([
    prisma.auditLog.aggregate({ where: { hashVersion: { gte: 2 } }, _min: { id: true } }),
    prisma.auditLog.aggregate({ where: { hashVersion: { gte: 3 } }, _min: { id: true } }),
  ]);
  const v2FromId = c2._min.id;
  const v3FromId = c3._min.id;
  const erwarteteVersion = (id: number): 1 | 2 | 3 => {
    if (v3FromId !== null && id >= v3FromId) return 3;
    if (v2FromId !== null && id >= v2FromId) return 2;
    return 1;
  };

  // Dokumentierte Loeschungen einlesen – NUR aus signierten, selbst gueltigen
  // Traegerzeilen (Pentest R171-01).
  //
  // Vorher wurde jedem Manifest bedingungslos geglaubt. Das Manifest steht in
  // `changesAfter`, und dieses Feld ist erst ab Hash-Version 2 mitgehasht – auf
  // V1-Altzeilen also voellig unauthentifiziert. Ein Angreifer konnte daher in
  // eine BELIEBIGE V1-Zeile ein erfundenes Manifest schreiben, ohne deren Hash
  // zu veraendern, und damit eigene Loeschungen und Luecken als "erklaert"
  // ausweisen. Das hebelte zugleich die Eskalation an signierten Zeilen aus –
  // also Anker UND Versionsgrenze.
  //
  // Deshalb zaehlt ein Manifest nur, wenn seine Traegerzeile
  //   1. laut Versionsgrenze Stufe 3 sein MUSS und dies auch deklariert, und
  //   2. ihre HMAC-Signatur mit einem konfigurierten Schluessel aufgeht.
  // Ohne Schluessel gibt es keine gueltigen Traeger – dann bleiben Luecken
  // unerklaert, was die sichere Richtung ist.
  const deletionRanges: Array<{ from: number; to: number }> = [];
  const manifestKandidaten = await prisma.auditLog.findMany({
    where: { resourceType: 'AuditLog', action: 'DELETE', endpoint: '/api/audit-logs/cleanup' },
  });
  const manifestSchluessel = [auditHmacKey(), ...auditHmacKeysOld()].filter(
    (k): k is string => !!k,
  );
  for (const row of manifestKandidaten) {
    if (erwarteteVersion(row.id) !== 3 || row.hashVersion < 3) continue;
    if (manifestSchluessel.length === 0) continue;
    const signaturOk = manifestSchluessel.some(
      (k) => row.hash === generateHashV3(row as unknown as AuditHashV2Input, k),
    );
    if (!signaturOk) continue;
    try {
      const parsed = JSON.parse(row.changesAfter || '{}');
      for (const m of parsed.manifest || []) {
        if (typeof m.fromId === 'number' && typeof m.toId === 'number') {
          deletionRanges.push({ from: m.fromId, to: m.toId });
        }
      }
    } catch {
      // Unlesbares Manifest = nicht erklaerend; Luecke bleibt unerklaert.
    }
  }
  const gapErklaert = (prevId: number, curId: number) =>
    deletionRanges.some((r) => r.from <= curId && r.to >= prevId);

  // ---------------------------------------------------------------------
  // Protokollierte Neuberechnungen (Rehash) einsammeln.
  //
  // Ein Rehash macht die Kette rechnerisch stimmig - auch dort, wo vorher
  // Loeschungen als Luecken sichtbar waren. Genau das ist auf Staging passiert:
  // zwei Cleanups mit Aufbewahrung 0 entfernten 3155 Eintraege, der folgende
  // Rehash liess rund 700 Kettenluecken verschwinden, und die Pruefung meldete
  // danach „unveraendert und lueckenlos verkettet“. Wahr - und trotzdem das
  // Gegenteil dessen, was ein Leser mitnimmt.
  //
  // Umgekehrte Beweislast als bei Manifest und Siegel: Dort zaehlen NUR
  // signierte Traeger, weil ein gefaelschter Marker Luecken wegerklaeren
  // koennte - Misstrauen ist die sichere Richtung. Hier erzeugt ein Marker eine
  // WARNUNG. Wuerden wir nur signierte gelten lassen, koennte jemand einen
  // Rehash unsichtbar machen, indem er dessen Signatur zerstoert. Deshalb
  // zaehlt hier jeder auswertbare Marker; ob er signiert ist, wird nur
  // mitgeteilt.
  const rehashKandidaten = await prisma.auditLog.findMany({
    where: { resourceType: 'AuditLog', action: 'UPDATE', endpoint: '/api/audit-logs/rehash' },
    orderBy: { id: 'asc' },
  });
  const rehashSchluessel = [auditHmacKey(), ...auditHmacKeysOld()].filter(
    (k): k is string => !!k,
  );
  const rehashes: Array<{
    id: number;
    zeitpunkt: string;
    neuBerechnet: number | null;
    signiert: boolean;
    vorbefund: { manipuliert: number; luecken: number } | null;
  }> = [];
  for (const row of rehashKandidaten) {
    let neuBerechnet: number | null = null;
    let vorbefund: { manipuliert: number; luecken: number } | null = null;
    try {
      const nach = JSON.parse(row.changesAfter || '{}');
      if (typeof nach.neuBerechnet !== 'number') continue; // kein Rehash-Marker
      neuBerechnet = nach.neuBerechnet;
      const vor = JSON.parse(row.changesBefore || '{}');
      if (Array.isArray(vor.manipuliert) && Array.isArray(vor.ketten_luecken)) {
        vorbefund = {
          manipuliert: vor.manipuliert.length,
          luecken: vor.ketten_luecken.length,
        };
      }
    } catch {
      continue;
    }
    const signiert =
      row.hashVersion >= 3 &&
      rehashSchluessel.some(
        (k) => row.hash === generateHashV3(row as unknown as AuditHashV2Input, k),
      );
    rehashes.push({
      id: row.id,
      zeitpunkt: row.createdAt.toISOString(),
      neuBerechnet,
      signiert,
      vorbefund,
    });
  }

  const tamperedEntries: number[] = [];
  const chainGaps: number[] = [];
  const unexplainedGaps: number[] = [];
  const unverifiableEntries: number[] = [];

  // ---------------------------------------------------------------------
  // Bestandssiegel pruefen (Pentest R171-02)
  //
  // Der Altbestand ist nicht signierbar: bei Version 1 sind nur 7 von 24
  // Spalten gehasht. Wurde er versiegelt, wird hier je Zeile geprueft:
  // vorhanden, immer noch Altbestand (hashVersion < 3) und Inhalt unveraendert.
  // Der Marker muss selbst signiert und gueltig sein - sonst waere das Siegel
  // ueber denselben Weg faelschbar wie zuvor die Manifeste (R171-01).
  const backlogTampered: number[] = [];
  const backlogMissing: number[] = [];
  // Luecken, die das Siegel als bereits vorhanden beglaubigt (siehe unten).
  const beglaubigteLuecken = new Set<number>();
  let backlogSealStatus: 'kein_siegel' | 'intakt' | 'leer' | 'gebrochen' | 'entfernt' | 'nicht_noetig' = 'kein_siegel';
  let backlogSealRoot: string | null = null;

  const siegelKandidaten = await prisma.auditLog.findMany({
    where: { resourceType: BACKLOG_SEAL_RESOURCE, endpoint: BACKLOG_SEAL_ENDPOINT },
    orderBy: { id: 'desc' },
  });
  const siegelSchluessel = [auditHmacKey(), ...auditHmacKeysOld()].filter(
    (k): k is string => !!k,
  );
  // Nur signierte Marker MIT auswertbarem Inhalt zaehlen. Der zweite Teil ist
  // die Absicherung gegen Zeilen, die zufaellig dieselbe Signatur tragen, aber
  // gar kein Siegel beschreiben (Pentest R173-01).
  const hatSiegelInhalt = (r: { changesAfter: string | null }) => {
    try {
      const m = JSON.parse(r.changesAfter || '{}');
      return typeof m.toId === 'number' && typeof m.root === 'string' && m.root.length > 0;
    } catch {
      return false;
    }
  };
  const gueltigeSiegel = siegelSchluessel.length
    ? siegelKandidaten.filter(
        (r) =>
          erwarteteVersion(r.id) === 3 &&
          r.hashVersion >= 3 &&
          hatSiegelInhalt(r) &&
          siegelSchluessel.some((k) => r.hash === generateHashV3(r as unknown as AuditHashV2Input, k)),
      )
    : [];
  const siegel = gueltigeSiegel[0];
  const backlogSealCount = gueltigeSiegel.length;

  // Gegen-Check (Pentest R174-01): Der Siegelzustand hing ausschliesslich am
  // Marker – und den kann ein DB-Schreiber OHNE Schluessel loeschen. Danach
  // meldete die Pruefung `kein_siegel`, also Entwarnung, ununterscheidbar von
  // „nie versiegelt“. Steht der Marker am Ketten-Ende (genau der Zustand
  // direkt nach dem einmaligen Siegeln beim Deploy), riss dabei nicht einmal
  // eine Luecke: das Ergebnis war `valid: true` trotz gefaelschter gesiegelter
  // Zeile. Die Blaetter blieben verwaist zurueck und wurden nie konsultiert.
  //
  // Deshalb gilt jetzt: Blaetter vorhanden, aber kein gueltiger Marker =
  // Siegel ENTFERNT und damit ein Befund – nicht „nie versiegelt“.
  const blattAnzahl = await prisma.auditBacklogSeal.count();
  const unsignierteAnzahl = await prisma.auditLog.count({ where: { hashVersion: { lt: 3 } } });
  if (!siegel && blattAnzahl > 0 && siegelSchluessel.length > 0) {
    backlogSealStatus = 'entfernt';
  } else if (!siegel && blattAnzahl === 0 && unsignierteAnzahl === 0) {
    // Es gibt gar keinen Altbestand: Entweder ist alles signiert, oder das Log
    // beginnt erst mit der Signierung. Dann ist „nicht versiegelt“ kein Mangel
    // (Pentest R183-03) – die bisherige Warnung liess sich nicht aufloesen,
    // weil seal-backlog zu Recht ablehnte. Eine Warnung, die der Betreiber
    // nicht beheben kann, lernt er zu ignorieren.
    //
    // Die Bedingung haengt bewusst an der Zahl der UNSIGNIERTEN Zeilen und
    // nicht mehr an `v3FromId === null`. Letzteres bedeutet naemlich das
    // genaue Gegenteil: dass ueberhaupt nichts signiert ist – etwa weil
    // AUDIT_HMAC_KEY fehlt. Ein vollstaendig unsigniertes Log meldete damit
    // „Bestandssiegel nicht noetig“, also Entwarnung fuer genau den Zustand
    // mit der geringsten Beweiskraft. Jetzt bleibt es bei „kein_siegel“; die
    // Fehlermeldung von seal-backlog nennt den fehlenden Schluessel als
    // naechsten Schritt, die Warnung ist also aufloesbar.
    backlogSealStatus = 'nicht_noetig';
  }

  if (siegel) {
    try {
      const meta = JSON.parse(siegel.changesAfter || '{}');
      const bis: number = meta.toId;
      const blaetter = await prisma.auditBacklogSeal.findMany();
      const erwartet = new Map(blaetter.map((b) => [b.auditLogId, b.leafHash]));

      const altzeilen = await prisma.auditLog.findMany({
        where: { id: { lte: bis } },
        orderBy: { id: 'asc' },
      });
      const vorhanden = new Map(altzeilen.map((r) => [r.id, r]));

      for (const [id, blatt] of erwartet) {
        const zeile = vorhanden.get(id);
        if (!zeile) {
          // FEHLENDE gesiegelte Zeile ist ein Siegelbruch, keine blosse Luecke
          // (Pentest R173-02). Vorher wurde hier uebersprungen: das Loeschen
          // eines Einbruchsbelegs erschien nur als "unerklaerte Luecke" –
          // ununterscheidbar von den als meist harmlos dokumentierten
          // Race-Luecken –, waehrend der dedizierte Indikator "intakt" meldete.
          backlogMissing.push(id);
          continue;
        }
        if (zeile.hashVersion >= 3 || backlogLeaf(zeile as unknown as Record<string, unknown>) !== blatt) {
          backlogTampered.push(id);
        }
      }

      // Wurzel gegenrechnen: faengt auch Zeilen ab, die dem Siegel
      // nachtraeglich hinzugefuegt oder daraus entfernt wurden.
      const wurzelJetzt = backlogRoot(blaetter);
      backlogSealRoot = meta.root;
      backlogSealStatus =
        backlogTampered.length === 0 && backlogMissing.length === 0 && wurzelJetzt === meta.root
          ? 'intakt'
          : 'gebrochen';

      // Ein Siegel ueber NULL Blaettern ist rechnerisch tadellos und schuetzt
      // nichts: es gab zum Zeitpunkt des Siegelns keinen Altbestand. „intakt“
      // zu melden waere formal richtig und trotzdem irrefuehrend - es liest
      // sich als Schutzzusage. Der Pentester hat Stagings Leersiegel genau so
      // missverstanden, und das ist der rote Faden im Kleinen: ein Signal, das
      // beruhigt, wo nichts abgesichert ist. Eigener Zustand, gleiche Wertung
      // wie „nicht noetig“ - kein Befund, aber auch keine Zusage.
      if (backlogSealStatus === 'intakt' && blaetter.length === 0) {
        backlogSealStatus = 'leer';
      }

      // ---------------------------------------------------------------
      // Beglaubigte Alt-Luecken
      //
      // Die Race-Luecken aus der Zeit vor dem Sperr-Fix lassen sich nicht
      // mehr heilen: die Verkettung ist gebrochen, die Inhalte sind aber
      // unversehrt. Wuerden sie `valid` dauerhaft auf false halten, meldete
      // das Gegenbuch stuendlich Alarm, ohne dass es je etwas zu tun gaebe -
      // und genau daran stirbt jede Warnung. Ein Signal, das immer schreit,
      // warnt nicht mehr.
      //
      // Deshalb gilt eine Luecke als BEGLAUBIGT, wenn beides zutrifft:
      //   1. sie liegt im versiegelten Bereich (id <= toId), und
      //   2. sie steht im Vorbefund des Siegel-Markers, also im Zustand, den
      //      der Betreiber beim Siegeln ausdruecklich festgeschrieben hat.
      // Der Vorbefund liegt in `changesBefore` und ist ab Version 3 mit-
      // gehasht - die Liste laesst sich also ohne Schluessel nicht nachtraeg-
      // lich erweitern (gleiche Absicherung wie beim Manifest, R171-01).
      //
      // Beglaubigt heisst NICHT verschwunden: die Luecken bleiben in
      // `chainGaps` und werden weiter berichtet. Sie zaehlen nur nicht mehr
      // als offener Befund. Alles andere schlaegt unveraendert an - eine
      // NEUE Luecke, eine veraenderte oder entfernte Altzeile, ein gebroche-
      // nes Siegel. Bei nicht intaktem Siegel wird gar nichts beglaubigt.
      if (backlogSealStatus === 'intakt') {
        try {
          const roh = siegel.changesEncrypted
            ? decrypt(siegel.changesBefore || '')
            : siegel.changesBefore || '{}';
          const vorbefund = JSON.parse(roh);
          for (const id of vorbefund?.befund?.ketten_luecken || []) {
            if (typeof id === 'number' && id <= bis) beglaubigteLuecken.add(id);
          }
        } catch {
          // Unlesbarer Vorbefund beglaubigt nichts - die sichere Richtung.
        }
      }
    } catch {
      backlogSealStatus = 'gebrochen';
    }
  }
  tamperedEntries.push(...backlogTampered, ...backlogMissing);

  // ---------------------------------------------------------------------
  // Fehlender ANFANG des Protokolls.
  //
  // Die Verkettung wird zeilenweise gegen die Vorgaengerin geprueft - die
  // erste Zeile hat keine, also fiel bisher NICHTS auf, wenn ein
  // zusammenhaengender Anfang entfernt wurde. Kein Kettenbruch, kein Befund,
  // `valid` blieb gruen. Das ist die stillste Loeschung von allen: Wer die
  // aeltesten Eintraege loswerden will, muss nur vorne anfangen.
  //
  // Erkennbar ist es trotzdem: Die allererste Zeile eines Protokolls wird ohne
  // Vorgaenger geschrieben und traegt deshalb einen leeren `previousHash`.
  // Traegt die erste vorhandene Zeile einen Wert, hat es eine Vorgaengerin
  // gegeben - und die ist weg.
  //
  // Nur bei Pruefung des GESAMTEN Bereichs; bei `fromId` ist ein gefuellter
  // `previousHash` selbstverstaendlich und kein Befund.
  if (fromId === undefined && logs.length > 0 && logs[0].previousHash) {
    chainGaps.push(logs[0].id);
    // Ein protokolliertes Loeschungs-Manifest erklaert auch diese Luecke; die
    // Pruefung laeuft ueber denselben Weg wie bei jeder anderen.
    if (!gapErklaert(0, logs[0].id)) {
      unexplainedGaps.push(logs[0].id);
      if (erwarteteVersion(logs[0].id) === 3) {
        tamperedEntries.push(logs[0].id);
      }
    }
  }

  for (let i = 0; i < logs.length; i++) {
    const log = logs[i];

    // Erwartete Pruefstaerke aus der Kette, NICHT aus der Selbstauskunft.
    const erwartet = erwarteteVersion(log.id);
    if (log.hashVersion !== erwartet) {
      // Deklarierte Version passt nicht zur Position in der Kette →
      // Downgrade-Versuch (oder manipulierte Version).
      tamperedEntries.push(log.id);
      continue;
    }

    // Pruefverfahren richtet sich nach der erwarteten Version:
    //  3 = HMAC-signiert (Schluessel ausserhalb der DB)
    //  2 = SHA-256 ueber alle Inhaltsspalten
    //  1 = SHA-256 ueber 7 Felder, zusaetzlich historische Serialisierung
    let hashOk: boolean;
    if (erwartet >= 2) {
      const inhalt = {
        userId: log.userId,
        userEmail: log.userEmail,
        userRole: log.userRole,
        customerId: log.customerId,
        isCustomerPortal: log.isCustomerPortal,
        action: log.action,
        sensitivity: log.sensitivity,
        resourceType: log.resourceType,
        resourceId: log.resourceId,
        resourceLabel: log.resourceLabel,
        endpoint: log.endpoint,
        httpMethod: log.httpMethod,
        ipAddress: log.ipAddress,
        userAgent: log.userAgent,
        changesBefore: log.changesBefore,
        changesAfter: log.changesAfter,
        changesEncrypted: log.changesEncrypted,
        dataSubjectId: log.dataSubjectId,
        legalBasis: log.legalBasis,
        success: log.success,
        errorMessage: log.errorMessage,
        durationMs: log.durationMs,
        createdAt: log.createdAt,
        previousHash: log.previousHash,
      };

      if (erwartet === 3) {
        // HMAC-signiert: ohne JEDEN Schluessel ist die Zeile nicht pruefbar.
        // Das als "manipuliert" zu melden waere ein Fehlalarm – daher eigener
        // Topf. Ein FALSCHER Schluessel ist dagegen nicht von einer Faelschung
        // zu unterscheiden und wird bewusst als Befund gemeldet.
        const kandidaten = [auditHmacKey(), ...auditHmacKeysOld()].filter(
          (k): k is string => !!k,
        );
        if (kandidaten.length === 0) {
          unverifiableEntries.push(log.id);
          continue;
        }
        hashOk = kandidaten.some((k) => log.hash === generateHashV3(inhalt, k));
      } else {
        hashOk = log.hash === generateHashV2(inhalt);
      }
    } else {
      const v1 = {
        userEmail: log.userEmail,
        action: log.action,
        resourceType: log.resourceType,
        resourceId: log.resourceId,
        endpoint: log.endpoint,
        createdAt: log.createdAt,
        previousHash: log.previousHash,
      };
      hashOk = log.hash === generateHash(v1) || log.hash === generateHashLegacy(v1);
    }

    if (!hashOk) {
      tamperedEntries.push(log.id);
      continue;
    }

    // Prüfen ob previousHash mit dem Hash des vorherigen Eintrags übereinstimmt
    if (i > 0) {
      const previousLog = logs[i - 1];
      if (log.previousHash !== previousLog.hash) {
        chainGaps.push(log.id);
        if (!gapErklaert(previousLog.id, log.id)) {
          unexplainedGaps.push(log.id);
          // Ist die AKTUELLE Zeile HMAC-signiert, laesst sich ihr
          // `previousHash` ohne Schluessel nicht nachziehen. Eine unerklaerte
          // Luecke davor bedeutet dann zwingend: der Vorgaenger wurde
          // veraendert oder entfernt. Das ist kein struktureller Zufall,
          // sondern ein Befund.
          //
          // Dieser Pfad schliesst zugleich die Restluecke der Versionsgrenze:
          // Wird die FRUEHESTE V3-Zeile herabgestuft, wandert `MIN(id)` mit und
          // die Grenze allein wuerde den Downgrade durchwinken – der Nachfolger
          // erzeugt aber eine nicht faelschbare Luecke.
          if (erwartet === 3) {
            tamperedEntries.push(log.id);
          }
        }
      }
    }
  }

  // Eskalierte Luecken stehen sowohl in tamperedEntries als auch in chainGaps;
  // ohne Entdopplung zaehlte dieselbe Zeile zweimal (Pentest R171-03).
  //
  // Beglaubigte Alt-Luecken zaehlen NICHT als offener Befund (siehe die
  // Begruendung an `beglaubigteLuecken`). Sie bleiben in `chainGaps`
  // sichtbar und stehen zusaetzlich in `attestedGaps`.
  const attestedGaps = chainGaps.filter((id) => beglaubigteLuecken.has(id));
  const offeneLuecken = chainGaps.filter((id) => !beglaubigteLuecken.has(id));
  const invalidEntries = [...new Set([...tamperedEntries, ...offeneLuecken])].sort((a, b) => a - b);

  // Ein gebrochenes oder entferntes Siegel muss `valid` kippen, auch wenn keine
  // einzelne Zeile beanstandet ist – sonst bliebe der stille Anker-Verlust
  // unsichtbar (R174-01).
  const siegelInOrdnung =
    backlogSealStatus === 'intakt' ||
    backlogSealStatus === 'leer' ||
    backlogSealStatus === 'kein_siegel' ||
    backlogSealStatus === 'nicht_noetig';

  return {
    valid: invalidEntries.length === 0 && siegelInOrdnung,
    checkedCount: logs.length,
    invalidEntries,
    tamperedEntries,
    chainGaps,
    unexplainedGaps,
    unverifiableEntries,
    attestedGaps,
    rehashes,
    backlogSealStatus,
    backlogTampered,
    backlogMissing,
    backlogSealCount,
    backlogSealRoot,
  };
}

/**
 * Hash-Kette komplett neu berechnen (Reparatur)
 */
export async function rehashAll(
  ausgeloestVon?: { userEmail?: string; ipAddress?: string },
): Promise<{ rehashedCount: number }> {
  // ZUSTAND VOR DEM REHASH SICHERN (Pentest R170-01)
  //
  // Ein Rehash macht die Kette rechnerisch wieder stimmig – auch dann, wenn sie
  // vorher berechtigte Beanstandungen enthielt. Der bisherige Marker hielt nur
  // fest, DASS rehasht wurde, nicht WAS dabei verschwand. Wer `audit:admin`
  // besitzt, konnte damit Spuren glattziehen, ohne dass hinterher erkennbar
  // war, welche.
  //
  // Deshalb wird der Befund samt Kettenkopf VOR dem Rehash erhoben und im
  // Marker mitgeschrieben. Der Marker entsteht nach dem Rehash, ist selbst
  // Teil der neuen Kette und signiert; entfernen liesse er sich nur unter
  // Hinterlassung einer Luecke.
  const vorher = await verifyIntegrity();
  const kopf = await prisma.auditLog.findFirst({
    orderBy: { id: 'desc' },
    select: { id: true, hash: true, hashVersion: true, createdAt: true },
  });

  const logs = await prisma.auditLog.findMany({
    orderBy: { id: 'asc' },
    select: {
      id: true,
      userId: true,
      userEmail: true,
      userRole: true,
      customerId: true,
      isCustomerPortal: true,
      action: true,
      sensitivity: true,
      resourceType: true,
      resourceId: true,
      resourceLabel: true,
      endpoint: true,
      httpMethod: true,
      ipAddress: true,
      userAgent: true,
      changesBefore: true,
      changesAfter: true,
      changesEncrypted: true,
      dataSubjectId: true,
      legalBasis: true,
      success: true,
      errorMessage: true,
      durationMs: true,
      createdAt: true,
    },
  });

  const rehashKey = auditHmacKey();
  let previousHash: string | null = null;
  let count = 0;

  for (const log of logs) {
    // Rehash schreibt die hoechste verfuegbare Stufe – ein Rueckfall auf eine
    // schwaechere Version wuerde die Abdeckung nachtraeglich verkleinern.
    const inhalt: AuditHashV2Input = { ...log, previousHash };
    const hash: string = rehashKey ? generateHashV3(inhalt, rehashKey) : generateHashV2(inhalt);

    await prisma.auditLog.update({
      where: { id: log.id },
      data: { hash, previousHash, hashVersion: rehashKey ? 3 : 2 },
    });

    previousHash = hash;
    count++;
  }

  // Marker: Ein Rehash setzt die Beweiskraft der Vergangenheit zurueck (jede
  // vorhandene Faelschung wuerde mitbesiegelt). Der Vorgang muss deshalb im
  // Log selbst sichtbar sein. Der Eintrag wird NACH dem Rehash geschrieben und
  // haengt sich an die neu berechnete Kette; entfernen liesse er sich nur unter
  // Hinterlassung einer Luecke.
  await createAuditLog({
    userEmail: ausgeloestVon?.userEmail || 'system',
    userRole: 'System',
    action: 'UPDATE',
    resourceType: 'AuditLog',
    resourceLabel:
      `Hash-Kette neu berechnet (${count} Einträge) – Beweiskraft der Vergangenheit zurückgesetzt` +
      (vorher.valid
        ? ' – Kette war vorher unbeanstandet'
        : ` – vorher beanstandet: ${vorher.tamperedEntries.length} manipuliert, ` +
          `${vorher.chainGaps.length} Lücken`),
    endpoint: '/api/audit-logs/rehash',
    httpMethod: 'POST',
    ipAddress: ausgeloestVon?.ipAddress || 'system',
    sensitivity: 'CRITICAL',
    // Befund VOR dem Rehash – ohne das waere nach dem Rehash nicht mehr
    // nachvollziehbar, was uebertuencht wurde.
    changesBefore: {
      geprueft: vorher.checkedCount,
      manipuliert: vorher.tamperedEntries,
      ketten_luecken: vorher.chainGaps,
      luecken_ohne_dokumentierte_loeschung: vorher.unexplainedGaps,
      nicht_pruefbar: vorher.unverifiableEntries,
      kettenkopf: kopf
        ? { id: kopf.id, hash: kopf.hash, hashVersion: kopf.hashVersion, createdAt: kopf.createdAt }
        : null,
    },
    changesAfter: { neuBerechnet: count },
    success: true,
  });

  return { rehashedCount: count };
}

/**
 * Exportiert Audit-Logs als JSON oder CSV
 */
export async function exportAuditLogs(
  params: AuditLogSearchParams,
  format: 'json' | 'csv' = 'json'
): Promise<string> {
  // Alle Logs ohne Paginierung
  const result = await searchAuditLogs({ ...params, limit: 100000, page: 1 });
  const logs = result.data;

  if (format === 'json') {
    return JSON.stringify(logs, null, 2);
  }

  // CSV Export
  const headers = [
    'ID',
    'Zeitstempel',
    'Benutzer',
    'Aktion',
    'Ressource',
    'Ressource-ID',
    'Bezeichnung',
    'Endpoint',
    'IP-Adresse',
    'Erfolg',
    'Sensitivität',
  ];

  const rows = logs.map((log) => [
    log.id.toString(),
    log.createdAt.toISOString(),
    log.userEmail,
    log.action,
    log.resourceType,
    log.resourceId || '',
    log.resourceLabel || '',
    log.endpoint,
    log.ipAddress,
    log.success ? 'Ja' : 'Nein',
    log.sensitivity,
  ]);

  const csvContent = [
    headers.join(';'),
    ...rows.map((row) => row.map((cell) => `"${cell.replace(/"/g, '""')}"`).join(';')),
  ].join('\n');

  return csvContent;
}

/**
 * Löscht alte Audit-Logs basierend auf Retention-Policies
 * Hinweis: Diese Funktion sollte nur von einem autorisierten Admin-Prozess aufgerufen werden
 */
export async function runRetentionCleanup(): Promise<{
  deletedCount: number;
  policies: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }>;
}> {
  const policies = await prisma.auditRetentionPolicy.findMany({
    where: { isActive: true },
  });

  const results: Array<{ resourceType: string; sensitivity: string | null; deletedCount: number }> = [];
  let totalDeleted = 0;

  // Loeschungs-Manifest (Pentest R166, Design): Jede geloeschte Zeile reisst die
  // Hash-Kette auf. Ohne Nachweis, WELCHE Bereiche legitim entfernt wurden,
  // koennte sich eine boeswillige Loeschung als "harmloser Gap" tarnen. Deshalb
  // halten wir Bereich + Anzahl fest und schreiben sie als eigenen, selbst
  // wieder verketteten Audit-Eintrag. Luecken ausserhalb dieser Bereiche
  // bleiben damit erklaerungsbeduerftig.
  const manifest: Array<Record<string, unknown>> = [];

  for (const policy of policies) {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - policy.retentionDays);

    const where: Prisma.AuditLogWhereInput = {
      createdAt: { lt: cutoffDate },
    };

    if (policy.resourceType !== '*') {
      where.resourceType = policy.resourceType;
    }

    if (policy.sensitivity) {
      where.sensitivity = policy.sensitivity;
    }

    // Betroffenen ID-Bereich VOR dem Loeschen festhalten.
    const range = await prisma.auditLog.aggregate({
      where,
      _count: true,
      _min: { id: true },
      _max: { id: true },
    });

    const deleted = await prisma.auditLog.deleteMany({ where });

    if (deleted.count > 0) {
      manifest.push({
        resourceType: policy.resourceType,
        sensitivity: policy.sensitivity,
        retentionDays: policy.retentionDays,
        cutoff: cutoffDate.toISOString(),
        deletedCount: deleted.count,
        fromId: range._min.id,
        toId: range._max.id,
      });
    }

    results.push({
      resourceType: policy.resourceType,
      sensitivity: policy.sensitivity,
      deletedCount: deleted.count,
    });

    totalDeleted += deleted.count;
  }

  if (totalDeleted > 0) {
    await createAuditLog({
      userEmail: 'system',
      userRole: 'System',
      action: 'DELETE',
      resourceType: 'AuditLog',
      resourceLabel: `Retention-Cleanup: ${totalDeleted} Audit-Einträge gelöscht`,
      endpoint: '/api/audit-logs/cleanup',
      httpMethod: 'POST',
      ipAddress: 'system',
      sensitivity: 'CRITICAL',
      changesAfter: { manifest },
      success: true,
    });
  }

  return {
    deletedCount: totalDeleted,
    policies: results,
  };
}

/**
 * Holt die Retention-Policies
 */
export async function getRetentionPolicies() {
  return prisma.auditRetentionPolicy.findMany({
    orderBy: [{ resourceType: 'asc' }, { sensitivity: 'asc' }],
  });
}

/**
 * Aktualisiert eine Retention-Policy
 */
export async function updateRetentionPolicy(
  id: number,
  data: { retentionDays?: number; description?: string; legalBasis?: string; isActive?: boolean }
) {
  return prisma.auditRetentionPolicy.update({
    where: { id },
    data,
  });
}
