/**
 * Registrierung der IPC-Handler – ausschließlich über den typisierten Vertrag
 * aus `src/shared/ipc.ts`.
 *
 * Die Typisierung beschreibt, was der Renderer schicken SOLL. Was er
 * tatsächlich schickt, ist an dieser Grenze unbekannt: die Nutzlasten werden
 * deshalb in den Fachmodulen (`katalog.ts`, `lernstand.ts`) zur Laufzeit
 * geprüft, bevor irgendetwas damit geschieht.
 */

import { join } from 'node:path';

import { app, BrowserWindow, clipboard, dialog, ipcMain, type IpcMainInvokeEvent } from 'electron';

import {
  IPC_KANAELE,
  KANAL_HILFSMITTEL_GEAENDERT,
  type AnwendungsInfo,
  type IpcAnfrage,
  type IpcAntwort,
  type IpcKanal,
  type Katalogwechsel,
} from '../shared/ipc';
import { anzeigegroesseSetzen } from './anzeige';
import { dateiZeigen, ordnerZeigen } from './dateizugriff';
import { datenbankStatus } from './datenbank';
import { einstellungenLesen, einstellungenSchreiben } from './einstellungen';
import { erklaerungenLaden } from './erklaerungen';
import { glossarLaden } from './glossar';
import { normtexteLaden } from './normtexte';
import { themenLaden } from './themen';
import { katalogBild, katalogLaden } from './katalog';
import { fehlerprotokollExportieren, fragenlisteExportieren } from './fragendruck-export';
import { lernberichtExportieren } from './lernbericht-export';
import {
  datenbankKonstruktor,
  istBeschaedigt,
  lernstandBeiseitelegen,
  lernstandInstanz,
  lernstandOffen,
  lernstandSchliessen,
  type Lernstand,
} from './lernstand';
import { protokollPfad } from './protokoll';
import {
  kopieVorDemVerwerfen,
  selbstsicherungAnlegen,
  selbstsicherungsordner,
} from './selbstsicherung';
import { datenschutzLesen } from './datenschutz';
import { lizenzenLesen } from './lizenzen';
import { pruefungInstanz, pruefungZuruecksetzen, type Pruefung } from './pruefung';
import {
  sicherungAnlegen,
  sicherungEinspielen,
  sicherungUebernehmen,
  sicherungPruefen,
  type Umgebung as Sicherungsumgebung,
} from './sicherung-dialoge';
import { laufMelden } from './laufwaechter';
import { istErlaubteNavigation, unterstuetzungOeffnen } from './sicherheit';

/** Dateiname des Lernstands im `userData`-Verzeichnis. */
const LERNSTAND_DATEI = 'lernstand.db';

/**
 * Anwendungsversion, zur Bauzeit aus der package.json eingesetzt
 * (siehe `electron.vite.config.ts`).
 *
 * `app.getVersion()` taugt dafür nicht durchgängig: Wird der gebaute
 * Einstiegspunkt direkt gestartet – in der Entwicklung und in den
 * Oberflächentests – findet Electron keine package.json und liefert
 * ersatzweise seine eigene Version zurück.
 */
declare const __ANWENDUNGS_VERSION__: string | undefined;

function anwendungsVersion(): string {
  return typeof __ANWENDUNGS_VERSION__ === 'string' ? __ANWENDUNGS_VERSION__ : app.getVersion();
}

/**
 * Baukennung und Commit-Datum, ebenfalls zur Bauzeit eingesetzt.
 *
 * Ohne Git beim Bauen bleiben beide leer; die Oberfläche lässt die Zeile
 * dann weg, statt „unbekannt“ anzuzeigen.
 */
declare const __BAUKENNUNG__: string | undefined;
declare const __BAUSTAND__: string | undefined;

function zeichenkette(wert: string | undefined): string {
  return typeof wert === 'string' ? wert : '';
}

/**
 * Stammt die Anfrage aus unserer eigenen Oberfläche?
 *
 * Zweite Verteidigungslinie hinter der Navigationssperre in `sicherheit.ts`.
 * Käme eine fremde Seite doch einmal in den Renderer, hätte sie über das
 * Preload-Skript die volle Bridge in der Hand – hier endet das trotzdem.
 *
 * Ein Rahmen ohne URL wird durchgelassen: Das kommt vor, wenn der Rahmen
 * gerade abgebaut wird, und wäre kein Angriff, sondern ein Wettlauf.
 */
function vonEigenerOberflaeche(ereignis: IpcMainInvokeEvent): boolean {
  const herkunft = ereignis.senderFrame?.url;
  return herkunft === undefined || herkunft === '' || istErlaubteNavigation(herkunft);
}

/**
 * Typsichere Hülle um `ipcMain.handle`: Kanal, Anfrage- und Antworttyp werden
 * aus {@link IpcVertrag} abgeleitet, sodass Tippfehler zu Compilerfehlern
 * werden statt zu stillen `undefined`-Antworten.
 */
function behandeln<K extends IpcKanal>(
  kanal: K,
  /* Auch ein Versprechen ist erlaubt: `ipcMain.handle` wartet es ab. Gebraucht
     wird das vom Export, der einen Dialog zeigt und danach schreibt. */
  behandler: (
    ereignis: IpcMainInvokeEvent,
    anfrage: IpcAnfrage<K>,
  ) => IpcAntwort<K> | Promise<IpcAntwort<K>>,
): void {
  ipcMain.handle(kanal, (ereignis, anfrage: unknown) => {
    if (!vonEigenerOberflaeche(ereignis)) {
      console.warn(`[sicherheit] IPC-Anfrage fremder Herkunft abgewiesen: ${kanal}`);
      throw new Error('Diese Anfrage stammt nicht aus der Oberfläche dieser Anwendung.');
    }
    return behandler(ereignis, anfrage as IpcAnfrage<K>);
  });
}

/**
 * Holt ein Feld aus einer IPC-Nutzlast. Trotz Typisierung wird hier nichts
 * vorausgesetzt – die eigentliche Prüfung der Werte macht das Fachmodul.
 */
function feld(anfrage: unknown, name: string): unknown {
  if (typeof anfrage !== 'object' || anfrage === null || Array.isArray(anfrage)) {
    throw new Error(`Ungültige Anfrage: Es wurde ein Objekt mit dem Feld „${name}“ erwartet.`);
  }
  return (anfrage as Record<string, unknown>)[name];
}

/**
 * Befund des Katalogstand-Abgleichs. Der Abgleich selbst läuft beim Öffnen
 * des Lernstands; hier wird er nur abgeholt. Scheitert das Öffnen – etwa bei
 * einem Lernstand aus einer neueren Programmversion oder ohne Katalog –,
 * antwortet die Systemauskunft trotzdem: Diese Fehler haben ihre eigenen
 * Meldungen an anderer Stelle.
 */
function katalogwechselLesen(): Katalogwechsel | null {
  try {
    return lernstand().katalogwechsel;
  } catch {
    return null;
  }
}

function anwendungsInfo(): AnwendungsInfo {
  return {
    anwendungsVersion: anwendungsVersion(),
    baukennung: zeichenkette(__BAUKENNUNG__),
    baustand: zeichenkette(__BAUSTAND__),
    electronVersion: process.versions.electron,
    chromeVersion: process.versions.chrome,
    nodeVersion: process.versions.node,
    plattform: process.platform,
    datenbank: datenbankStatus(),
    katalogwechsel: katalogwechselLesen(),
    protokollPfad: protokollPfad(),
  };
}

/**
 * Lernstand beim ersten Bedarf öffnen. Der Katalog wird dabei mitgeladen –
 * ohne ihn lassen sich weder Fragen zusammenstellen noch Statistiken bilden.
 *
 * **Der eine Fall, in dem es hier etwas anzubieten gibt.** Ist die Datei
 * beschädigt, endete das bis 0.22.0 in einer Sackgasse: Der Renderer meldete
 * „Ihr Lernprofil konnte nicht geladen werden … Antworten werden dabei nicht
 * gespeichert“, und dabei blieb es bei jedem Start aufs Neue. Wer nicht von
 * sich aus auf die Idee kam, im `userData`-Verzeichnis nachzusehen, konnte
 * die Anwendung nie wieder benutzen.
 *
 * Deshalb fragt der Hauptprozess hier nach. Der Dialog ist bewusst
 * synchron und nativ: Er muss vorliegen, bevor dieser Aufruf zurückkehrt,
 * und er wird auch dann noch vorgelesen, wenn die Oberfläche nichts anzeigen
 * kann. Nichts wird von selbst getan – die beschädigte Datei bleibt liegen,
 * bis jemand ausdrücklich zustimmt, und auch dann wird sie nur umbenannt.
 */
function lernstand(): Lernstand {
  const pfad = join(app.getPath('userData'), LERNSTAND_DATEI);
  try {
    return lernstandInstanz(pfad, katalogLaden());
  } catch (fehler: unknown) {
    if (!istBeschaedigt(fehler)) {
      throw fehler;
    }
    return beschaedigtenLernstandKlaeren(pfad, fehler);
  }
}

/**
 * Bietet nach einer beschädigten Datei den Neuanfang an – einmal je Start.
 *
 * Der Merker verhindert eine Dialogschleife: Jeder IPC-Kanal, der einen
 * Lernstand braucht, liefe sonst in dieselbe Rückfrage, und der Startbildschirm
 * fragt mehrere davon gleich nacheinander ab.
 */
let beschaedigungGeklaert = false;

function beschaedigtenLernstandKlaeren(pfad: string, fehler: unknown): Lernstand {
  if (beschaedigungGeklaert) {
    throw fehler;
  }
  beschaedigungGeklaert = true;

  const wahl = dialog.showMessageBoxSync({
    type: 'error',
    buttons: ['Beiseitelegen und neu anfangen', 'Nichts tun'],
    defaultId: 1,
    cancelId: 1,
    noLink: true,
    title: 'Waffensachkunde – Lernsoftware',
    message: 'Die Datei mit Ihrem Lernstand ist beschädigt.',
    detail: [
      'Sie lässt sich nicht mehr öffnen. Gelöscht wird nichts: Auf Wunsch wird die beschädigte Datei umbenannt und beiseitegelegt, und die Anwendung beginnt mit einem leeren Lernstand.',
      'Haben Sie eine Sicherung, können Sie sie danach unter „Lernstand sichern und übertragen“ einspielen.',
      `Die beschädigte Datei bleibt in jedem Fall erhalten unter:\n${pfad}`,
    ].join('\n\n'),
  });

  if (wahl !== 0) {
    throw fehler;
  }

  const beiseite = lernstandBeiseitelegen(pfad);
  console.warn(`[lernstand] Beschädigte Datei beiseitegelegt: ${beiseite}`);
  return lernstandInstanz(pfad, katalogLaden());
}

/**
 * Die Prüfungssimulation arbeitet auf demselben Lernstand: ein Lauf soll in
 * Statistik und Wiedervorlage einfließen, nicht daneben stehen.
 */
function pruefung(): Pruefung {
  return pruefungInstanz(lernstand(), katalogLaden());
}

export function ipcRegistrieren(): void {
  behandeln('anwendung:info', () => anwendungsInfo());
  behandeln('einstellungen:lesen', () => einstellungenLesen());
  behandeln('einstellungen:schreiben', (_ereignis, einstellungen) =>
    einstellungenSchreiben(einstellungen),
  );

  /* Electron meldet über `accessibilitySupportEnabled`, ob eine assistive
     Technologie erkannt wurde. Die Oberfläche richtet danach die Voreinstellung
     der Zeichenkürzel aus – sie kollidieren im Lesemodus von NVDA und JAWS mit
     deren Schnellnavigation. */
  behandeln('system:hilfsmittel', () => app.accessibilitySupportEnabled);

  /*
    Die Zwischenablage liegt im Hauptprozess, weil der Renderer sie nicht
    bekommt: `sicherheit.ts` lehnt sämtliche Berechtigungen ab, und Blink
    fragt für `navigator.clipboard.writeText()` eine ab. Nachgemessen im
    gebauten Fenster – `isSecureContext` true, Aufruf trotzdem
    `NotAllowedError: Write permission denied`.

    Die Obergrenze ist kein Misstrauen gegen die eigene Oberfläche, sondern
    dieselbe Sorgfalt wie bei jeder anderen Nutzlast: Was über die Brücke
    kommt, wird geprüft, bevor es wirkt. Der Meldeblock ist rund 500 Zeichen
    lang; 20 000 lassen jede vorstellbare Erweiterung zu und schließen aus,
    dass ein Fehler anderswo die Zwischenablage des Nutzers mit Unrat füllt.
  */
  behandeln('system:kopieren', (_ereignis, anfrage) => {
    const text = feld(anfrage, 'text');
    if (typeof text !== 'string' || text.length === 0 || text.length > 20_000) {
      return false;
    }
    clipboard.writeText(text);
    return true;
  });

  /*
    Der einzige Kanal, der nach außen führt. Er nimmt eine Adresse entgegen,
    damit prüfbar bleibt, dass die Oberfläche genau die anfordert, die sie
    anzeigt – geöffnet wird aber ausschließlich die eine eingetragene, siehe
    `sicherheit.ts`. Ohne Eintrag öffnet er nichts, und die Oberfläche zeigt
    das Angebot dann gar nicht erst an.
  */
  behandeln('system:unterstuetzung', (_ereignis, anfrage) =>
    unterstuetzungOeffnen(feld(anfrage, 'url')),
  );

  /*
    Öffnet ausschließlich, was dieser Prozess in dieser Sitzung selbst
    geschrieben hat. Der Renderer schickt eine Kennung, keinen Pfad – wer
    einen Pfad mitbringen darf, darf auch einen anderen mitbringen.
  */
  behandeln('system:datei-zeigen', async (_ereignis, anfrage) => {
    const wunsch = feld(anfrage, 'wunsch');
    return dateiZeigen(feld(anfrage, 'kennung'), wunsch === 'ordner' ? 'ordner' : 'oeffnen');
  });

  /* Kein Argument: Der Pfad wird hier gebildet und nicht entgegengenommen.
     Ein Kanal, der einen Ordner entgegennähme, wäre ein Weg, beliebige
     Ordner des Rechners zu öffnen. */
  behandeln('sicherung:ordner-zeigen', () =>
    ordnerZeigen(selbstsicherungsordner(app.getPath('userData'))),
  );

  behandeln('ansicht:groesse', (_ereignis, anfrage) =>
    anzeigegroesseSetzen(feld(anfrage, 'prozent')),
  );

  behandeln('katalog:laden', () => katalogLaden());
  behandeln('katalog:bild', (_ereignis, anfrage) => katalogBild(feld(anfrage, 'bildId')));
  behandeln('erklaerungen:laden', () => erklaerungenLaden());
  behandeln('glossar:laden', () => glossarLaden());
  behandeln('gesetz:normtexte', () => normtexteLaden());
  behandeln('themen:laden', () => themenLaden());

  behandeln('profil:liste', () => lernstand().profile());
  behandeln('profil:anlegen', (_ereignis, anfrage) =>
    lernstand().profilAnlegen(feld(anfrage, 'name')),
  );
  behandeln('profil:aktualisieren', (_ereignis, anfrage) =>
    lernstand().profilAktualisieren(anfrage),
  );
  behandeln('profil:loeschen', (_ereignis, anfrage) => {
    /* Erst die Kopie, dann das Löschen: Misslingt sie, wird nichts
       gelöscht – der Fehler geht an die Oberfläche. */
    kopieVorVerwerfen();
    return lernstand().profilLoeschen(feld(anfrage, 'id'));
  });

  behandeln('lernen:sitzung', (_ereignis, anfrage) =>
    lernstand().sitzung(feld(anfrage, 'profilId'), feld(anfrage, 'filter')),
  );
  behandeln('lernen:antworten', (_ereignis, anfrage) =>
    lernstand().antworten(feld(anfrage, 'profilId'), feld(anfrage, 'protokoll')),
  );
  behandeln('lernen:merken', (_ereignis, anfrage) =>
    lernstand().merken(
      feld(anfrage, 'profilId'),
      feld(anfrage, 'frageId'),
      feld(anfrage, 'gemerkt'),
    ),
  );
  behandeln('lernen:fragestand', (_ereignis, anfrage) =>
    lernstand().frageStand(feld(anfrage, 'profilId'), feld(anfrage, 'frageId')),
  );
  behandeln('lernen:hartnaeckige', (_ereignis, anfrage) =>
    lernstand().hartnaeckige(feld(anfrage, 'profilId')),
  );
  behandeln('lernen:uebersicht', (_ereignis, anfrage) =>
    lernstand().uebersicht(feld(anfrage, 'profilId')),
  );
  behandeln('lernen:plan', (_ereignis, anfrage) => lernstand().lernplan(feld(anfrage, 'profilId')));
  behandeln('lernen:zuruecksetzen', (_ereignis, anfrage) => {
    kopieVorVerwerfen();
    return lernstand().zuruecksetzen(feld(anfrage, 'profilId'), feld(anfrage, 'kapitel'));
  });

  /*
    Sicherung des Lernstands. Drei Kanäle, weil zwischen „das steht in der
    Datei" und „ja, ersetze meinen Lernstand" eine Entscheidung des Nutzers
    liegt: Der Prüfschritt ist folgenlos, erst `einspielen` fasst etwas an.

    Die Umgebung wird bei jedem Aufruf frisch gebaut – `lernstand()` muss beim
    Einspielen NACH dem Ersetzen eine neue Instanz liefern, und ein
    festgehaltener Verweis zeigte auf die geschlossene alte.
  */
  const sicherungsumgebung = (): Sicherungsumgebung => ({
    lernstand,
    katalog: katalogLaden,
    Datenbank: datenbankKonstruktor(),
    jetzt: () => new Date(),
  });

  behandeln('sicherung:anlegen', async (ereignis) =>
    sicherungAnlegen(BrowserWindow.fromWebContents(ereignis.sender), sicherungsumgebung()),
  );

  behandeln('sicherung:pruefen', async (ereignis) =>
    sicherungPruefen(BrowserWindow.fromWebContents(ereignis.sender), sicherungsumgebung()),
  );

  behandeln('sicherung:einspielen', (ereignis, anfrage) =>
    sicherungEinspielen(
      feld(anfrage, 'vorgang'),
      BrowserWindow.fromWebContents(ereignis.sender),
      sicherungsumgebung(),
    ),
  );

  /* Ohne `async`, wie das Einspielen: Der Hauptprozess ist einfädig, und
     solange nichts abgewartet wird, kann sich kein zweiter Kanal
     dazwischenschieben und in derselben Datenbank schreiben. */
  behandeln('sicherung:uebernehmen', (ereignis, anfrage) =>
    sicherungUebernehmen(
      feld(anfrage, 'vorgang'),
      feld(anfrage, 'profilIndex'),
      BrowserWindow.fromWebContents(ereignis.sender),
      sicherungsumgebung(),
    ),
  );

  /* Der Speicherdialog hängt am Fenster, aus dem der Auftrag kam – modal ist
     für Screenreader die klarere Lage als ein freischwebender Dialog. */
  behandeln('druck:lernbericht', async (ereignis, anfrage) =>
    lernberichtExportieren(
      {
        lernstand: lernstand(),
        pruefung: pruefung(),
        katalog: katalogLaden(),
        elternfenster: BrowserWindow.fromWebContents(ereignis.sender),
        jetzt: () => new Date(),
      },
      feld(anfrage, 'profilId'),
      feld(anfrage, 'schriftgroesse'),
    ),
  );

  behandeln('druck:fehlerprotokoll', async (ereignis, anfrage) =>
    fehlerprotokollExportieren(
      {
        lernstand: lernstand(),
        katalog: katalogLaden(),
        elternfenster: BrowserWindow.fromWebContents(ereignis.sender),
        jetzt: () => new Date(),
      },
      feld(anfrage, 'profilId'),
      feld(anfrage, 'schriftgroesse'),
      feld(anfrage, 'tiefe'),
    ),
  );

  behandeln('druck:fragenliste', async (ereignis, anfrage) =>
    fragenlisteExportieren(
      {
        lernstand: lernstand(),
        katalog: katalogLaden(),
        elternfenster: BrowserWindow.fromWebContents(ereignis.sender),
        jetzt: () => new Date(),
      },
      feld(anfrage, 'bereiche'),
      feld(anfrage, 'schriftgroesse'),
      feld(anfrage, 'mitLoesungen'),
    ),
  );

  behandeln('pruefung:starten', (_ereignis, anfrage) =>
    pruefung().starten(feld(anfrage, 'profilId'), feld(anfrage, 'auftrag')),
  );
  behandeln('pruefung:auswerten', (_ereignis, anfrage) =>
    pruefung().auswerten(
      feld(anfrage, 'profilId'),
      feld(anfrage, 'auftrag'),
      feld(anfrage, 'antworten'),
      feld(anfrage, 'dauerMs'),
      feld(anfrage, 'zeitAbgelaufen'),
    ),
  );
  behandeln('pruefung:sichern', (_ereignis, anfrage) =>
    pruefung().sichern(feld(anfrage, 'profilId'), feld(anfrage, 'stand')),
  );
  behandeln('pruefung:offen', (_ereignis, anfrage) =>
    pruefung().offenerBogen(feld(anfrage, 'profilId')),
  );
  behandeln('pruefung:verwerfen', (_ereignis, anfrage) => {
    pruefung().verwerfen(feld(anfrage, 'profilId'));
    return undefined;
  });
  behandeln('pruefung:laeuft', (ereignis, anfrage) => {
    /* An den sendenden `webContents` gebunden, nicht an ein Modul-Merkmal:
       Sonst spräche ein zweites Fenster für das erste mit. */
    const laeuft = feld(anfrage, 'laeuft');
    laufMelden(ereignis.sender, laeuft === true);
    return undefined;
  });
  behandeln('pruefung:verlauf', (_ereignis, anfrage) =>
    pruefung().verlauf(feld(anfrage, 'profilId')),
  );

  behandeln('lizenzen:lesen', () => lizenzenLesen());
  behandeln('datenschutz:lesen', () => datenschutzLesen());
}

/**
 * Meldet Änderungen des Hilfsmittel-Zustands an alle Fenster.
 *
 * Ein Screenreader kann auch mitten in der Sitzung gestartet werden – gerade
 * dann, wenn jemand merkt, dass er ihn braucht. Ohne diese Meldung bliebe die
 * Oberfläche auf dem Stand des Programmstarts.
 */
function hilfsmittelMelden(_ereignis: unknown, aktiv: boolean): void {
  for (const fenster of BrowserWindow.getAllWindows()) {
    if (!fenster.isDestroyed()) {
      fenster.webContents.send(KANAL_HILFSMITTEL_GEAENDERT, aktiv);
    }
  }
}

export function hilfsmittelBeobachten(): void {
  /* Benannt und nicht anonym, damit `ipcAbmelden` ihn wieder lösen kann.
     Anonym ließ er sich nicht entfernen: `ipcAbmelden` heißt „Gegenstück für
     Tests und sauberes Herunterfahren“ und hielt das bis Fassung 0.24.1 für
     diesen einen Zuhörer nicht ein. In der Auslieferung folgenlos – der
     Aufruf geschieht genau einmal –, aber ein Gegenstück, das nicht alles
     zurücknimmt, ist beim nächsten Mal die Falle. */
  app.on('accessibility-support-changed', hilfsmittelMelden);
}

/**
 * Legt beim Beenden eine Sicherheitskopie an, wenn eine fällig ist.
 *
 * **Vor** dem Schließen: `VACUUM INTO` braucht die offene Verbindung. Und
 * nur, wenn der Lernstand in dieser Sitzung überhaupt geöffnet wurde – wer
 * das Programm startet und gleich wieder beendet, hat nichts zu sichern.
 *
 * Ein Fehlschlag hält das Beenden nicht auf. Eine Anwendung, die sich wegen
 * einer misslungenen Nebensache nicht schließen lässt, wäre der schlechtere
 * Handel; die Kopie ist eine Zugabe, kein Versprechen.
 */
/**
 * Sicherheitskopie vor einem Schritt, der Daten vernichtet.
 *
 * Misslingt sie, wirft sie – und `behandeln` reicht den Fehler an die
 * Oberfläche durch, ohne dass der Schritt ausgeführt wird. Das ist der
 * Unterschied zur Kopie beim Beenden: Dort ist sie eine Zugabe, hier ist sie
 * die Bedingung.
 */
function kopieVorVerwerfen(): void {
  const offen = lernstandOffen();
  if (offen === null) {
    return;
  }
  const pfad = kopieVorDemVerwerfen(
    offen.datenbank,
    app.getPath('userData'),
    datenbankKonstruktor(),
  );
  console.info(`[sicherung] Sicherheitskopie vor dem Verwerfen: ${pfad}`);
}

function selbstsicherungBeimBeenden(): void {
  const offen = lernstandOffen();
  if (offen === null) {
    return;
  }

  try {
    const pfad = selbstsicherungAnlegen(
      offen.datenbank,
      app.getPath('userData'),
      datenbankKonstruktor(),
    );
    if (pfad !== null) {
      console.info(`[sicherung] Selbsttätige Sicherheitskopie: ${pfad}`);
    }
  } catch (fehler: unknown) {
    console.warn('[sicherung] Selbsttätige Sicherheitskopie misslang:', fehler);
  }
}

/** Gegenstück für Tests und sauberes Herunterfahren. */
export function ipcAbmelden(): void {
  for (const kanal of IPC_KANAELE) {
    ipcMain.removeHandler(kanal);
  }
  app.off('accessibility-support-changed', hilfsmittelMelden);
  pruefungZuruecksetzen();
  selbstsicherungBeimBeenden();
  lernstandSchliessen();
}
