/**
 * Preload-Skript – die einzige Brücke zwischen Renderer und Main-Prozess.
 *
 * Läuft mit `sandbox: true` und `contextIsolation: true`. Deshalb gilt:
 *  - keine Node-APIs außer `electron`, `events`, `timers`, `url`,
 *  - das Bundle muss vollständig gebündelt sein (keine externen Importe),
 *  - Ausgabeformat CommonJS (`out/preload/index.js`).
 *
 * Der Renderer bekommt ausschließlich die Funktionen aus
 * {@link LernAppBridge} – niemals `ipcRenderer` selbst.
 */

import { contextBridge, ipcRenderer } from 'electron';

import type { Uebernahmeergebnis } from '../shared/sicherung';
import type { Erklaerungstiefe } from '../shared/druck/fehlerprotokoll';
import type { Schriftgroesse } from '../shared/druck/stil';
import type { Erklaerungen } from '../shared/erklaerungen';
import type { Glossar } from '../shared/glossar';
import type { Normtexte } from '../shared/normtexte';
import type { Themen } from '../shared/themen';
import type { HartnaeckigeFrage } from '../shared/hartnaeckig';
import type { Katalog } from '../shared/katalog';
import type { Lernplan } from '../shared/lernplan';
import {
  BRIDGE_NAME,
  istIpcKanal,
  istMenuebefehl,
  KANAL_ANZEIGEGROESSE_GEAENDERT,
  KANAL_ENERGIESPAREN_ENDE,
  KANAL_HILFSMITTEL_GEAENDERT,
  KANAL_MENUE_BEFEHL,
  type AnwendungsInfo,
  type Menuebefehl,
  type Druckergebnis,
  type Einstellungen,
  type IpcAnfrage,
  type IpcAntwort,
  type IpcKanal,
  type LernAppBridge,
} from '../shared/ipc';
import type {
  Antwortprotokoll,
  FrageStand,
  Lernuebersicht,
  Profil,
  SitzungsFilter,
  SitzungsFrage,
} from '../shared/lernstand';
import type { Datenschutzangaben } from '../shared/datenschutz';
import type { Lizenzangaben } from '../shared/lizenzen';
import type { Einspielergebnis, Pruefergebnis, Sicherungsergebnis } from '../shared/sicherung';
import type {
  OffenerLauf,
  Pruefungsantwort,
  Pruefungsauftrag,
  Pruefungsbogen,
  Pruefungsergebnis,
  Pruefungsverlauf,
  Zwischenstand,
} from '../shared/pruefung';

/**
 * Typsicherer Aufruf. Die Kanalprüfung ist eine zusätzliche Verteidigungslinie:
 * selbst wenn Renderer-Code manipuliert würde, kommen nur Kanäle aus der
 * Allowlist durch.
 */
async function aufrufen<K extends IpcKanal>(
  kanal: K,
  anfrage: IpcAnfrage<K>,
): Promise<IpcAntwort<K>> {
  if (!istIpcKanal(kanal)) {
    throw new Error(`Unbekannter IPC-Kanal: ${String(kanal)}`);
  }
  return (await ipcRenderer.invoke(kanal, anfrage)) as IpcAntwort<K>;
}

const bridge: LernAppBridge = Object.freeze({
  anwendungsInfoLesen: (): Promise<AnwendungsInfo> => aufrufen('anwendung:info', undefined),
  einstellungenLesen: (): Promise<Einstellungen> => aufrufen('einstellungen:lesen', undefined),
  einstellungenSchreiben: (aenderung: Partial<Einstellungen>): Promise<Einstellungen> =>
    aufrufen('einstellungen:schreiben', aenderung),

  // ── Fragenkatalog ────────────────────────────────────────────────────
  katalogLaden: (): Promise<Katalog> => aufrufen('katalog:laden', undefined),
  /** Nur die ID wandert über die Brücke – nie ein Pfad. */
  katalogBild: (bildId: string): Promise<string> => aufrufen('katalog:bild', { bildId }),
  erklaerungenLaden: (): Promise<Erklaerungen> => aufrufen('erklaerungen:laden', undefined),
  glossarLaden: (): Promise<Glossar> => aufrufen('glossar:laden', undefined),
  normtexteLaden: (): Promise<Normtexte> => aufrufen('gesetz:normtexte', undefined),
  themenLaden: (): Promise<Themen> => aufrufen('themen:laden', undefined),

  // ── Profile ──────────────────────────────────────────────────────────
  profilListe: (): Promise<Profil[]> => aufrufen('profil:liste', undefined),
  profilAnlegen: (name: string): Promise<Profil> => aufrufen('profil:anlegen', { name }),
  profilAktualisieren: (
    id: number,
    aenderung: {
      name?: string;
      pruefungstermin?: string | null;
      kapitelAusschluss?: readonly string[];
    },
  ): Promise<Profil> => aufrufen('profil:aktualisieren', { id, ...aenderung }),

  // ── Lernen ───────────────────────────────────────────────────────────
  lernSitzung: (profilId: number, filter: SitzungsFilter): Promise<SitzungsFrage[]> =>
    aufrufen('lernen:sitzung', { profilId, filter }),
  lernAntworten: (profilId: number, protokoll: Antwortprotokoll): Promise<FrageStand> =>
    aufrufen('lernen:antworten', { profilId, protokoll }),
  lernMerken: (profilId: number, frageId: string, gemerkt: boolean): Promise<FrageStand> =>
    aufrufen('lernen:merken', { profilId, frageId, gemerkt }),
  lernFragestand: (profilId: number, frageId: string): Promise<FrageStand> =>
    aufrufen('lernen:fragestand', { profilId, frageId }),
  lernHartnaeckige: (profilId: number): Promise<HartnaeckigeFrage[]> =>
    aufrufen('lernen:hartnaeckige', { profilId }),
  lernUebersicht: (profilId: number): Promise<Lernuebersicht> =>
    aufrufen('lernen:uebersicht', { profilId }),
  lernPlan: (profilId: number): Promise<Lernplan> => aufrufen('lernen:plan', { profilId }),
  lernZuruecksetzen: (profilId: number, kapitel?: string | null): Promise<Lernuebersicht> =>
    aufrufen('lernen:zuruecksetzen', { profilId, kapitel: kapitel ?? null }),

  // ── Prüfungssimulation ───────────────────────────────────────────────
  pruefungStarten: (profilId: number, auftrag: Pruefungsauftrag): Promise<Pruefungsbogen> =>
    aufrufen('pruefung:starten', { profilId, auftrag }),
  /**
   * Die Richtigkeit der Antworten wird im Main-Prozess nachgerechnet – was
   * hier hinübergeht, sind ausschließlich die Eingaben des Prüflings.
   */
  pruefungAuswerten: (
    profilId: number,
    auftrag: Pruefungsauftrag,
    antworten: Pruefungsantwort[],
    dauerMs: number,
    zeitAbgelaufen: boolean,
  ): Promise<Pruefungsergebnis> =>
    aufrufen('pruefung:auswerten', { profilId, auftrag, antworten, dauerMs, zeitAbgelaufen }),
  pruefungVerlauf: (profilId: number): Promise<Pruefungsverlauf[]> =>
    aufrufen('pruefung:verlauf', { profilId }),
  pruefungSichern: (profilId: number, stand: Zwischenstand): Promise<boolean> =>
    aufrufen('pruefung:sichern', { profilId, stand }),
  pruefungOffen: (profilId: number): Promise<OffenerLauf | null> =>
    aufrufen('pruefung:offen', { profilId }),
  pruefungVerwerfen: (profilId: number): Promise<void> =>
    aufrufen('pruefung:verwerfen', { profilId }),
  pruefungLaeuft: (laeuft: boolean): Promise<void> => aufrufen('pruefung:laeuft', { laeuft }),

  // ── Rechtliches ──────────────────────────────────────────────────────
  anzeigegroesseSetzen: (prozent: number): Promise<number> =>
    aufrufen('ansicht:groesse', { prozent }),
  anzeigegroesseBeobachten: (melden: (prozent: number) => void): (() => void) => {
    const zuhoerer = (_ereignis: unknown, prozent: unknown): void => {
      if (typeof prozent === 'number') {
        melden(prozent);
      }
    };
    ipcRenderer.on(KANAL_ANZEIGEGROESSE_GEAENDERT, zuhoerer);
    return () => {
      ipcRenderer.off(KANAL_ANZEIGEGROESSE_GEAENDERT, zuhoerer);
    };
  },

  sicherungUebernehmen: (vorgang: string, profilIndex: number): Promise<Uebernahmeergebnis> =>
    aufrufen('sicherung:uebernehmen', { vorgang, profilIndex }),
  fehlerprotokollDrucken: (
    profilId: number,
    schriftgroesse: Schriftgroesse,
    tiefe: Erklaerungstiefe,
  ): Promise<Druckergebnis> =>
    aufrufen('druck:fehlerprotokoll', { profilId, schriftgroesse, tiefe }),
  fragenlisteDrucken: (
    bereiche: readonly string[],
    schriftgroesse: Schriftgroesse,
    mitLoesungen: boolean,
  ): Promise<Druckergebnis> =>
    aufrufen('druck:fragenliste', { bereiche, schriftgroesse, mitLoesungen }),
  lernberichtDrucken: (profilId: number, schriftgroesse: Schriftgroesse): Promise<Druckergebnis> =>
    aufrufen('druck:lernbericht', { profilId, schriftgroesse }),

  profilLoeschen: (id: number): Promise<Profil[]> => aufrufen('profil:loeschen', { id }),

  lizenzenLesen: (): Promise<Lizenzangaben> => aufrufen('lizenzen:lesen', undefined),
  datenschutzLesen: (): Promise<Datenschutzangaben> => aufrufen('datenschutz:lesen', undefined),

  hilfsmittelAktiv: (): Promise<boolean> => aufrufen('system:hilfsmittel', undefined),

  /* Nur die Zeichenkette wandert hinüber – der Renderer bestimmt den Inhalt,
     der Hauptprozess führt aus. Genau deshalb gibt es diesen Kanal statt
     einer gelockerten Berechtigung: Er hat eine feste Nutzlast. */
  zwischenablageSchreiben: (text: string): Promise<boolean> =>
    aufrufen('system:kopieren', { text }),

  /* Der einzige Weg nach außen – und er führt nicht irgendwohin: Der
     Hauptprozess vergleicht die Adresse mit der einen eingetragenen und
     öffnet sonst nichts. Hier steht deshalb bewusst keine Prüfung: Zwei
     Prüfungen an zwei Stellen laufen auseinander, und maßgeblich ist die
     im Hauptprozess. */
  unterstuetzungOeffnen: (url: string): Promise<boolean> =>
    aufrufen('system:unterstuetzung', { url }),

  /* Nur die Kennung wandert hinüber, nie ein Pfad – dieselbe Überlegung wie
     bei der Sicherung und beim unterbrochenen Prüfungsbogen. */
  dateiZeigen: (kennung: string, wunsch: 'oeffnen' | 'ordner'): Promise<boolean> =>
    aufrufen('system:datei-zeigen', { kennung, wunsch }),

  /* Ohne Argument – der Hauptprozess weiß, welcher Ordner gemeint ist. */
  sicherungsordnerZeigen: (): Promise<boolean> => aufrufen('sicherung:ordner-zeigen', undefined),

  sicherungAnlegen: (): Promise<Sicherungsergebnis> => aufrufen('sicherung:anlegen', undefined),
  sicherungPruefen: (): Promise<Pruefergebnis> => aufrufen('sicherung:pruefen', undefined),
  /* Nur die Kennung wandert hinüber, nie ein Pfad. */
  sicherungEinspielen: (vorgang: string): Promise<Einspielergebnis> =>
    aufrufen('sicherung:einspielen', { vorgang }),
  /* Der Renderer bekommt nur eine Rückruffunktion, nie das Ereignisobjekt –
     sonst reichte man ihm über `event.sender` doch wieder Electron-Interna. */
  hilfsmittelBeobachten: (melden: (aktiv: boolean) => void): (() => void) => {
    const zuhoerer = (_ereignis: unknown, aktiv: unknown): void => {
      melden(aktiv === true);
    };
    ipcRenderer.on(KANAL_HILFSMITTEL_GEAENDERT, zuhoerer);
    return () => {
      ipcRenderer.removeListener(KANAL_HILFSMITTEL_GEAENDERT, zuhoerer);
    };
  },

  /* Was nicht auf der Liste steht, kommt nicht durch: Die Prüfung im
     Preload ist dieselbe Verteidigungslinie wie die Kanal-Allowlist – der
     Renderer bekommt nur Befehle, die es wirklich gibt. */
  menuebefehlBeobachten: (melden: (befehl: Menuebefehl) => void): (() => void) => {
    const zuhoerer = (_ereignis: unknown, befehl: unknown): void => {
      if (istMenuebefehl(befehl)) {
        melden(befehl);
      }
    };
    ipcRenderer.on(KANAL_MENUE_BEFEHL, zuhoerer);
    return () => {
      ipcRenderer.removeListener(KANAL_MENUE_BEFEHL, zuhoerer);
    };
  },

  /* Nur eine Zahl wandert hinüber, und sie wird hier auf Brauchbarkeit
     geprüft: Eine negative oder unendliche Dauer verschöbe den Zeitursprung
     der Simulation ins Sinnlose. Der Hauptprozess prüft dasselbe – zwei
     Linien, wie bei den übrigen Kanälen. */
  energiesparenBeobachten: (melden: (schlafMs: number) => void): (() => void) => {
    const zuhoerer = (_ereignis: unknown, schlafMs: unknown): void => {
      if (typeof schlafMs === 'number' && Number.isFinite(schlafMs) && schlafMs > 0) {
        melden(schlafMs);
      }
    };
    ipcRenderer.on(KANAL_ENERGIESPAREN_ENDE, zuhoerer);
    return () => {
      ipcRenderer.removeListener(KANAL_ENERGIESPAREN_ENDE, zuhoerer);
    };
  },
});

if (process.contextIsolated) {
  contextBridge.exposeInMainWorld(BRIDGE_NAME, bridge);
} else {
  // Sollte nie eintreten – contextIsolation ist hart aktiviert.
  throw new Error('contextIsolation ist deaktiviert. Die Anwendung startet nicht ungesichert.');
}
