/**
 * Das Gerüst gedruckter Dokumente – reine Funktionen, ohne Electron und ohne DOM.
 *
 * Ein Dokument dieser Anwendung entsteht als vollständige, in sich
 * geschlossene HTML-Datei. Kein Skript, keine externe Adresse, kein Nachladen:
 * Was hier herauskommt, ist genau das, was gedruckt wird.
 *
 * **Die Quellenangabe ist kein Beiwerk.** Der amtliche Fragenkatalog ist ein
 * amtliches Werk; wer ihn wiedergibt, muss die Quelle nennen (§ 63 UrhG) und
 * darf ihn nicht ändern (§ 62 UrhG). Die Pflicht hängt an der
 * Vervielfältigung, nicht an der Weitergabe – ein PDF, das nur auf dem
 * eigenen Rechner liegt, ist davon nicht ausgenommen, und weitergeben lässt
 * es sich ohnehin jederzeit.
 *
 * Deshalb nimmt {@link dokumentBauen} die Quellenangabe als Pflichtfeld
 * entgegen. Es gibt keinen Schalter, sie wegzulassen, und keinen Pfad, auf
 * dem ein Dokument ohne sie entstehen könnte. Sie steht an zwei Stellen: als
 * Block am Anfang und – über die Fußzeile von `printToPDF` – auf jedem
 * einzelnen Blatt. Beides ist nötig, weil gedruckte Seiten getrennt werden.
 */

import { druckStil, type Schriftgroesse } from './stil';

/**
 * Maskiert Text für die Einbettung in HTML.
 *
 * Gilt für **jeden** Wert, der aus Daten stammt – auch für die aus dem
 * eigenen Katalog. Eine Ausnahme „das ist doch unser eigener Text“ hält
 * genau so lange, bis jemand ein Profil „Max & Moritz“ anlegt.
 */
export function maskiert(wert: string): string {
  return wert
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');
}

/** Angaben zur Herkunft, wie sie ins Dokument müssen. */
export interface Quellenangabe {
  /** Wortlaut aus `katalog.json`, unverändert. */
  readonly amtlich: string;
  /** Herausgeber des amtlichen Werks. */
  readonly herausgeber: string;
  /** Katalogstand als ISO-Datum. */
  readonly stand: string;
  /** Bezugsadresse – als Text, nie als Verweis. */
  readonly quelleUrl: string;
  /**
   * Wie viel des amtlichen Werks dieses Dokument wiedergibt.
   *
   * Hieß bis Fassung 0.18.0 schlicht `'auszug' | 'kein amtlicher wortlaut'`,
   * und der Satz zu `'auszug'` lautete unbedingt „Dieses Dokument gibt nur
   * einen Teil des amtlichen Fragenkatalogs wieder.“ Für eine Fragenliste
   * über den ganzen Katalog wäre das nachweislich falsch: 575 von 575
   * Fragen und 1430 von 1430 Antwortmöglichkeiten sind kein Teil, sondern
   * das Ganze.
   *
   * Deshalb wird jetzt **gezählt statt behauptet**. Aus den Zahlen bildet
   * {@link quellensatz} den Satz; ob er „alle“ oder „einen Teil“ sagt,
   * entscheidet die Zählung und nicht der Aufrufer.
   */
  readonly umfang: Werkumfang;
}

/**
 * Was ein Dokument vom amtlichen Werk wiedergibt – in Zahlen.
 *
 * Zwei Achsen, weil keine Formulierung beide trägt: wie viele **Fragen**
 * enthalten sind, und ob bei ihnen die **Lösung** ausgewiesen ist. Eine
 * Fragenliste über den ganzen Katalog ohne Lösungsanhang gibt alle Fragen
 * vollständig wieder und lässt zugleich jede Lösungsmarkierung weg; ein Satz,
 * der nur eine der beiden Achsen nennt, verschwiege die andere.
 */
export type Werkumfang =
  | {
      /** Das Dokument enthält keinen amtlichen Wortlaut (nur der Lernbericht). */
      readonly art: 'kein amtlicher wortlaut';
    }
  | {
      readonly art: 'wiedergabe';
      /** Wiedergegebene Fragen. */
      readonly fragen: number;
      /** Fragen im amtlichen Katalog insgesamt. */
      readonly fragenGesamt: number;
      /**
       * Sind die richtigen Antworten im Dokument ausgewiesen?
       *
       * `'alle'` – bei jeder Frage. `'keine'` – bei keiner; dann ist der
       * Bogen zum Bearbeiten gedacht. Ein Mittelding gibt es nicht: Beide
       * Dokumente entscheiden das für das ganze Dokument, nicht je Frage.
       */
      readonly loesungen: 'alle' | 'keine';
      /**
       * Sind bei Auswahlfragen alle Antwortmöglichkeiten abgedruckt?
       *
       * Ein Dokument, das nur die richtige Option zeigt, kürzt den amtlichen
       * Wortlaut erheblich – gemessen 801 von 1430 Optionstexten. Das muss
       * dastehen, sonst tritt das Dokument vollständiger auf, als es ist.
       */
      readonly optionen: 'alle' | 'nur die richtigen';
    };

export interface Dokumentbauplan {
  /** Erscheint als `<title>`, als `<h1>` und im Dateinamensvorschlag. */
  readonly titel: string;
  /** Kleine Zeile über dem Titel, etwa Profil und Datum. */
  readonly augenbraue: string;
  readonly quelle: Quellenangabe;
  /** Der Inhalt, bereits als HTML – jeder Abschnitt beginnt mit `<h2>`. */
  readonly abschnitte: readonly string[];
  readonly schriftgroesse: Schriftgroesse;
}

/**
 * Die Richtlinie des Druckdokuments.
 *
 * Strenger als die der Anwendung: Das Dokument braucht weder Skripte noch
 * Verbindungen, nur seinen eigenen eingebetteten Stil und – später, für die
 * Prüfzeichen – Bilder als `data:`-URI.
 *
 * Gemessen: Für `file://`-Dokumente greift in dieser Anwendung allein diese
 * `<meta>`-Angabe. Der Kopfzeilen-Weg aus `main/sicherheit.ts` wirkt dort
 * nicht – nachgewiesen daran, dass in einem `file://`-Fenster ohne eigene
 * Richtlinie sogar ein Inline-Skript lief. Umso wichtiger, dass sie hier
 * steht.
 */
const DRUCK_CSP =
  "default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'";

/**
 * Die Quellenangabe als Block.
 *
 * Der amtliche Wortlaut wird zitiert, nicht umformuliert. Die Adresse steht
 * als Text da und nicht als Verweis: Ein Dokument dieser Anwendung führt
 * nirgendwohin, und die Regel „keine externen Adressen“ bleibt damit prüfbar.
 */
/**
 * Die Sätze zum Umfang – aus der Zählung gebildet, nicht behauptet.
 *
 * Getrennt herausgezogen und einzeln geprüft, weil hier jede Ungenauigkeit
 * unmittelbar eine unwahre Aussage über ein amtliches Werk ergibt.
 */
export function quellensatz(umfang: Werkumfang): string {
  if (umfang.art === 'kein amtlicher wortlaut') {
    return (
      '<p>Dieses Dokument enthält keinen Wortlaut des amtlichen Fragenkatalogs; ' +
      'genannt werden lediglich dessen Kapitel- und Abschnittsbezeichnungen.</p>'
    );
  }

  const { fragen, fragenGesamt, loesungen, optionen } = umfang;
  const alle = fragen >= fragenGesamt;
  const eine = fragen === 1;

  /* „alle 575“ statt „575 der 575“: Wer den ganzen Katalog vor sich hat,
     soll das lesen und nicht selbst vergleichen müssen. Der Singular ist
     erreichbar – ein einziger Fehler ergibt ein Dokument mit einer Frage. */
  const menge = alle
    ? `alle ${String(fragenGesamt)} Fragen`
    : eine
      ? `1 der ${String(fragenGesamt)} Fragen`
      : `${String(fragen)} der ${String(fragenGesamt)} Fragen`;

  const zweiter =
    loesungen === 'alle'
      ? optionen === 'alle'
        ? 'Die Antwortmöglichkeiten sind vollständig wiedergegeben; die jeweils richtige ist gekennzeichnet.'
        : 'Von den Antwortmöglichkeiten ist nur die jeweils richtige wiedergegeben; die übrigen fehlen.'
      : 'Welche Antwort richtig ist, weist dieses Dokument nicht aus – die amtliche Kennzeichnung fehlt hier vollständig.';

  return (
    `<p>Dieses Dokument gibt ${menge} des amtlichen Fragenkatalogs wieder. ${zweiter}</p>\n` +
    '<p>Die Bildbeschreibungen zu den Prüf- und Zulassungszeichen stammen nicht aus dem ' +
    'amtlichen Fragenkatalog; sie sind eine Ergänzung dieser Software.</p>'
  );
}

function quellenblock(quelle: Quellenangabe): string {
  return `<section class="quelle" aria-labelledby="quelle-titel">
<h2 id="quelle-titel">Herkunft der Inhalte</h2>
<p><strong>Amtlicher Fragenkatalog:</strong> ${maskiert(quelle.amtlich)}</p>
<p>Herausgeber: ${maskiert(quelle.herausgeber)}. Stand: ${maskiert(quelle.stand)}.
Bezug: ${maskiert(quelle.quelleUrl)}</p>
${quellensatz(quelle.umfang)}
<p>Erklärungen, Bildbeschreibungen, Glossar und die Gestaltung dieses Dokuments sind
eigener Inhalt der Waffensachkunde-Lernsoftware, © 2026 Olaf Willerding, EUPL-1.2.</p>
</section>`;
}

/** Kurzform für die Fußzeile jeder Seite. */
export function kurzquelle(quelle: Quellenangabe): string {
  return `Amtlicher Fragenkatalog: ${quelle.herausgeber}, Stand ${quelle.stand} – wiedergegeben mit der Waffensachkunde-Lernsoftware`;
}

/**
 * Die Fußzeile jeder Seite, als Vorlage für `printToPDF`.
 *
 * Chromium ersetzt die Klassen `pageNumber` und `totalPages`. Die Vorlage
 * bekommt die Stile des Dokuments **nicht** mit und muss sie deshalb selbst
 * mitbringen; ohne eigene Größenangabe setzt Chromium sie winzig.
 */
export function fusszeilenVorlage(quelle: Quellenangabe): string {
  return (
    `<div style="width:100%;font-family:'Segoe UI',Arial,sans-serif;font-size:8pt;` +
    `color:#4a4f58;padding:0 12mm;display:flex;justify-content:space-between;gap:8mm;">` +
    `<span>${maskiert(kurzquelle(quelle))}</span>` +
    `<span>Seite <span class="pageNumber"></span> von <span class="totalPages"></span></span>` +
    `</div>`
  );
}

/** Leere Kopfzeile – ohne sie setzt Chromium seine eigene mit Titel und Adresse. */
export const KOPFZEILE_LEER = '<div></div>';

/**
 * Baut das vollständige Dokument.
 *
 * Die Struktur ist so gewählt, dass ein getaggtes PDF daraus etwas anfangen
 * kann: genau eine `h1`, darunter ausschließlich `h2` und `h3` ohne
 * Sprünge, Tabellen mit `caption` und `th`. Ob Chromium daraus wirklich
 * einen brauchbaren Strukturbaum macht, prüft der E2E-Lauf am erzeugten PDF
 * nach – behauptet wird es hier nicht.
 */
export function dokumentBauen(plan: Dokumentbauplan): string {
  if (plan.quelle.amtlich.trim().length === 0) {
    throw new Error('Ohne Quellenangabe wird kein Dokument erzeugt.');
  }

  return `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="${DRUCK_CSP}">
<title>${maskiert(plan.titel)}</title>
<style>${druckStil(plan.schriftgroesse)}</style>
</head>
<body>
<main>
<div class="kopf">
<p class="augenbraue">${maskiert(plan.augenbraue)}</p>
<h1>${maskiert(plan.titel)}</h1>
</div>
${quellenblock(plan.quelle)}
${plan.abschnitte.join('\n')}
</main>
</body>
</html>`;
}

// ─── Bausteine für die einzelnen Dokumente ──────────────────────────────

/** Eine Tabellenzelle: Text und ob sie eine Zahl trägt. */
export interface Zelle {
  readonly text: string;
  readonly zahl?: boolean;
  /** Zusätzliche Klasse, etwa `gut` oder `schlecht`. */
  readonly klasse?: string;
}

/**
 * Eine Tabelle mit Beschriftung und Kopfzeile.
 *
 * `caption` und `th scope` sind nicht Zierde: Ohne sie weiß ein Screenreader
 * beim Vorlesen einer Zelle nicht, wozu sie gehört, und im getaggten PDF
 * fehlt die Zuordnung ebenso.
 */
export function tabelle(
  beschriftung: string,
  kopf: readonly string[],
  zeilen: readonly (readonly Zelle[])[],
): string {
  const kopfzellen = kopf
    .map((text, i) => `<th scope="col"${i > 0 ? ' class="zahl"' : ''}>${maskiert(text)}</th>`)
    .join('');

  const koerper = zeilen
    .map((zeile) => {
      const zellen = zeile
        .map((z, i) => {
          const klassen = [z.zahl === true ? 'zahl' : '', z.klasse ?? ''].filter(Boolean).join(' ');
          const attribut = klassen === '' ? '' : ` class="${klassen}"`;
          return i === 0
            ? `<th scope="row"${attribut}>${maskiert(z.text)}</th>`
            : `<td${attribut}>${maskiert(z.text)}</td>`;
        })
        .join('');
      return `<tr>${zellen}</tr>`;
    })
    .join('\n');

  return `<table>
<caption>${maskiert(beschriftung)}</caption>
<thead><tr>${kopfzellen}</tr></thead>
<tbody>
${koerper}
</tbody>
</table>`;
}

/** Ein Abschnitt mit Überschrift und beliebigem Inhalt. */
export function abschnitt(titel: string, inhalt: readonly string[]): string {
  return `<section>\n<h2>${maskiert(titel)}</h2>\n${inhalt.join('\n')}\n</section>`;
}

/**
 * Eine eingebettete Abbildung.
 *
 * Der Alternativtext ist Pflicht und wird nicht aus dem Aufrufer geglaubt:
 * Ohne ihn entsteht kein Element, sondern ein sichtbarer Ersatztext. Das ist
 * die härtere Variante als ein leeres `alt` – im PDF-Strukturbaum landet eine
 * Abbildung ohne `/Alt` als stummes Kästchen, und wer das Dokument hört,
 * erführe an dieser Stelle gar nichts.
 *
 * Bei drei Fragen des Katalogs (3.05, 3.24, 39) sind die Prüfzeichen selbst
 * die Antwortmöglichkeiten; bei 3.05 haben zwei Optionen überhaupt keinen
 * Text. Ohne Einbettung stünde dort eine leere Zeile.
 */
export function bild(datenUrl: string, alt: string): string {
  const beschreibung = alt.trim();
  if (beschreibung.length === 0) {
    return absatz(
      '[Abbildung ohne Beschreibung – sie kann hier nicht wiedergegeben werden.]',
      'hinweis',
    );
  }
  if (!datenUrl.startsWith('data:image/')) {
    return absatz(`[Abbildung nicht lesbar: ${beschreibung}]`, 'hinweis');
  }
  return `<img src="${maskiert(datenUrl)}" alt="${maskiert(beschreibung)}">`;
}

/**
 * Ein Absatz aus **rohem** Text.
 *
 * Der Text wird hier maskiert und **nicht** vom Aufrufer. Das ist die
 * Richtung, die im Fehlerfall harmlos bleibt: Wer eine Maskierung vergisst,
 * bekommt eine doppelte, nicht eine fehlende. Umgekehrt wäre die vergessene
 * Maskierung eine Einschleusung.
 *
 * Daraus folgt die Regel für jeden Aufrufer: **kein `maskiert()` davor und
 * keine Auszeichnung darin.** Beides ging hier einmal schief – `frageblock.ts`
 * übergab `` `<strong>Kurz:</strong> ${maskiert(…)}` `` und erzeugte damit im
 * PDF den sichtbaren Text „&lt;strong&gt;Kurz:&lt;/strong&gt;“ und aus jedem
 * `&` ein „&amp;amp;“. Wer eine Beschriftung voranstellen will, nimmt
 * {@link absatzMitBeschriftung}; wer Auszeichnung braucht, baut das Element
 * hier und nicht beim Aufrufer.
 */
export function absatz(text: string, klasse?: string): string {
  const attribut = klasse === undefined ? '' : ` class="${klasse}"`;
  return `<p${attribut}>${maskiert(text)}</p>`;
}

/**
 * Ein Absatz mit fett vorangestellter Beschriftung: „**Kurz:** …“.
 *
 * Es gibt ihn, damit kein Aufrufer Auszeichnung in eine Zeichenkette
 * schreiben muss, die anschließend maskiert wird. Beide Teile kommen roh
 * herein und werden hier einzeln maskiert; das `<strong>` entsteht an der
 * einen Stelle, an der es entstehen darf.
 */
export function absatzMitBeschriftung(beschriftung: string, text: string, klasse?: string): string {
  const attribut = klasse === undefined ? '' : ` class="${klasse}"`;
  return `<p${attribut}><strong>${maskiert(beschriftung)}</strong> ${maskiert(text)}</p>`;
}
