/**
 * Die Handgriffe, die sich alle Druckdokumente teilen.
 *
 * Getrennt von `druck.ts` (HTML zu PDF) und von `shared/druck/` (reine
 * Textbausteine ohne Electron). Hier steht, was jedes Dokument gleich macht:
 * die Quellenangabe aus dem Katalog holen, den Speicherort erfragen, erst
 * danach rechnen, schreiben.
 *
 * **Erst fragen, dann rechnen.** Ein PDF zu erzeugen, das niemand haben will,
 * kostet nur Zeit – und beim schlimmsten gemessenen Fall sind das 982 Seiten.
 *
 * Was hier bewusst NICHT steht: eine gemeinsame Abstraktion über die
 * Dokumente selbst. Lernbericht, Fehlerprotokoll und Fragenliste bauen ihren
 * Inhalt jeweils selbst; geteilt werden nur die Handgriffe drumherum.
 */

import { renameSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

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

import type { Quellenangabe, Werkumfang } from '../shared/druck/dokument';
import type { Schriftgroesse } from '../shared/druck/stil';
import type { Druckergebnis } from '../shared/ipc';
import type { Frage, Katalog } from '../shared/katalog';
import type { Papierfrage } from '../shared/druck/frageblock';
import { geschriebenMerken } from './dateizugriff';
import { katalogBild } from './katalog';
import { pdfErzeugen } from './druck';

/**
 * Baut die Quellenangabe aus den Katalogmetadaten.
 *
 * Ohne Katalog gibt es keinen Export: Die Angabe ließe sich dann nicht
 * belegen, und ein Dokument mit erfundener oder fehlender Herkunft ist
 * schlechter als gar keines.
 */
export function quelleAusKatalog(katalog: Katalog, umfang: Werkumfang): Quellenangabe {
  const { quellenangabe, herausgeber, stand, quelle_url } = katalog.meta;
  if (quellenangabe.trim().length === 0) {
    throw new Error('Der Fragenkatalog nennt keine Quellenangabe – es wird nichts exportiert.');
  }
  return { amtlich: quellenangabe, herausgeber, stand, quelleUrl: quelle_url, umfang };
}

/** Bringt einen fremden Wert auf eine gültige Schriftgröße. */
export function schriftgroessePruefen(wert: unknown): Schriftgroesse {
  return wert === 'gross' ? 'gross' : 'normal';
}

/** Ein Zeitstempel, den Aufrufer setzen können – sonst wäre der Export untestbar. */
export type Zeitgeber = () => Date;

/**
 * Sammelt zu einer Frage alles, was das Papier braucht.
 *
 * Die Abbildungen werden hier eingebettet und nicht im Dokumentbaustein:
 * `katalogBild` liest von der Platte und kann scheitern. Ein unlesbares PNG
 * darf keinen 120-seitigen Export abreißen – die Frage erscheint dann mit
 * einem sichtbaren Ersatztext, und das Dokument sagt damit selbst, was fehlt.
 */
export function papierfragen(
  fragen: readonly Frage[],
  katalog: Katalog,
  alttexte: ReadonlyMap<string, string>,
  erklaerungen: ReadonlyMap<string, Papierfrage['erklaerung']>,
): Papierfrage[] {
  const kapitelTitel = new Map(katalog.kapitel.map((k) => [k.id, k.titel]));

  return fragen.map((frage) => {
    const ids = [...frage.bilder, ...(frage.optionen ?? []).flatMap((o) => o.bilder)];
    const bilder = new Map<string, string>();
    for (const id of ids) {
      try {
        bilder.set(id, katalogBild(id));
      } catch (fehler: unknown) {
        console.warn(`[druck] Abbildung „${id}“ nicht lesbar, sie fehlt im Dokument:`, fehler);
      }
    }
    return {
      frage,
      kapitelTitel: kapitelTitel.get(frage.kapitel) ?? frage.kapitel,
      bilder,
      alttexte,
      erklaerung: erklaerungen.get(frage.id),
    };
  });
}

export interface Speicherauftrag {
  /** Das fertige Dokument als HTML. */
  readonly html: string;
  readonly quelle: Quellenangabe;
  /** Titel der Dialogleiste, etwa „Fehlerprotokoll speichern“. */
  readonly dialogtitel: string;
  /** Vorgeschlagener Dateiname, ohne Pfad. */
  readonly dateiname: string;
  readonly elternfenster: BrowserWindow | null;
}

/**
 * Fragt nach dem Speicherort und schreibt die Datei.
 *
 * Gibt `gespeichert: false` zurück, wenn abgebrochen wurde – kein Fehler,
 * sondern eine Entscheidung des Nutzers.
 */
export async function speichern(auftrag: Speicherauftrag): Promise<Druckergebnis> {
  const vorschlag = join(app.getPath('documents'), auftrag.dateiname);
  const optionen: Electron.SaveDialogOptions = {
    title: auftrag.dialogtitel,
    defaultPath: vorschlag,
    buttonLabel: 'Speichern',
    filters: [{ name: 'PDF-Dokument', extensions: ['pdf'] }],
    /* Überschreiben nur nach Rückfrage, und ein neuer Ordner soll sich
       anlegen lassen – sonst muss der Nutzer den Dialog verlassen, um Platz
       für seine Datei zu schaffen. */
    properties: ['showOverwriteConfirmation', 'createDirectory'],
  };

  const auswahl = await (auftrag.elternfenster === null
    ? dialog.showSaveDialog(optionen)
    : dialog.showSaveDialog(auftrag.elternfenster, optionen));

  if (auswahl.canceled || auswahl.filePath === '') {
    return { gespeichert: false, pfad: null, bytes: 0 };
  }

  const pdf = await pdfErzeugen({ html: auftrag.html, quelle: auftrag.quelle });
  pdfSchreiben(auswahl.filePath, pdf);

  return {
    gespeichert: true,
    pfad: auswahl.filePath,
    bytes: pdf.byteLength,
    /* Erst nach dem Schreiben gemerkt: Was nicht existiert, soll sich auch
       nicht öffnen lassen. */
    dateiKennung: geschriebenMerken(auswahl.filePath),
  };
}

/**
 * Schreibt das PDF unteilbar: erst nach `.teil`, dann umbenennen.
 *
 * Dieselbe Reihenfolge wie bei der Sicherung (`main/sicherung.ts`), und aus
 * demselben Grund. Ein Fehlschlag mitten im Schreiben – voller Datenträger,
 * abgezogener Stick, ein Fehlerprotokoll über 982 Seiten – hinterließ bis
 * Fassung 0.24.1 eine halbe Datei unter dem endgültigen Namen. Sie ließ sich
 * anklicken, öffnete nicht, und die Anwendung hatte an derselben Stelle
 * gerade einen Fehler gemeldet: Wer nur den Dateimanager ansah, hielt sie für
 * das Ergebnis.
 *
 * Beim Ersetzen einer vorhandenen Datei gilt dasselbe wie dort: `renameSync`
 * tauscht sie unteilbar aus; bis dahin bleibt die alte stehen.
 */
export function pdfSchreiben(ziel: string, pdf: Uint8Array): void {
  const teil = `${ziel}.teil`;
  try {
    writeFileSync(teil, pdf);
    renameSync(teil, ziel);
  } catch (fehler: unknown) {
    try {
      rmSync(teil, { force: true });
    } catch {
      /* Wenn schon das Aufräumen scheitert, ist der Datenträger das
         Problem – gemeldet wird der ursprüngliche Fehler. */
    }
    throw fehler;
  }
}
