import { useCallback, useEffect, useRef, useState } from 'react';

import { HILFE_ZUR_ANSICHT } from '../../shared/hilfe';
import { SITZUNGSUMFANG } from '../../shared/lernplan';
import type { SitzungsFilter } from '../../shared/lernstand';
import type { Menuebefehl } from '../../shared/ipc';
import type { Pruefungsauftrag } from '../../shared/pruefung';
import { sprechtempoBereinigen } from '../../shared/sprechtempo';
import { KapitelWahl } from './components/lernen/KapitelWahl';
import { Lernsitzung } from './components/lernen/Lernsitzung';
import { Sitzungsende } from './components/lernen/Sitzungsende';
import { Glossaransicht } from './components/lernen/Glossaransicht';
import { Hilfedialog } from './components/hilfe/Hilfedialog';
import { Startbildschirm } from './components/lernen/Startbildschirm';
import { Suchansicht } from './components/suche/Suchansicht';
import { Pruefungsauswertung } from './components/pruefung/Pruefungsauswertung';
import { Pruefungslauf } from './components/pruefung/Pruefungslauf';
import { Pruefungswahl } from './components/pruefung/Pruefungswahl';
import { Profilfrage } from './components/profil/Profilfrage';
import { Zuschnittfrage } from './components/profil/Zuschnittfrage';
import { UeberSoftware } from './components/ueber/UeberSoftware';
import { Zurueckleiste } from './components/Zurueckleiste';
import { useEinstellungen } from './hooks/useEinstellungen';
import { useTextabstand } from './hooks/useTextabstand';
import { useHilfefenster } from './hooks/useHilfefenster';
import { useKatalog } from './hooks/useKatalog';
import { useGlossar } from './hooks/useGlossar';
import { useHartnaeckige } from './hooks/useHartnaeckige';
import { useLernplan } from './hooks/useLernplan';
import { useLernstand } from './hooks/useLernstand';
import { useOffenePruefung } from './hooks/useOffenePruefung';
import { useHilfsmittelAktiv } from './hooks/useHilfsmittel';
import { useKuerzelEinstellung } from './hooks/useTastenkuerzel';
import { useThema } from './hooks/useThema';
import { useZoomtasten } from './hooks/useZoomtasten';
import { lernBruecke } from './lernen/bruecke';
import type { Ansicht, SitzungsBilanz, Suchanfrage, Sitzungsquelle } from './lernen/typen';
import { KAPITEL_IV } from './pruefung/auftrag';
import type { Pruefungsabschluss } from './pruefung/typen';

export function App(): React.JSX.Element {
  const { auswahl, ansage, auswahlSetzen } = useThema();
  const { zustand: katalogZustand, neuLaden: katalogNeuLaden } = useKatalog();
  const lernstand = useLernstand();
  const lernplan = useLernplan(lernstand.profilId);
  const offenePruefung = useOffenePruefung(lernstand.profilId);
  const hartnaeckigeFragen = useHartnaeckige(lernstand.profilId);
  const glossar = useGlossar();
  const hilfsmittelAktiv = useHilfsmittelAktiv();
  const kuerzel = useKuerzelEinstellung(hilfsmittelAktiv);
  const einstellungen = useEinstellungen();
  const hilfe = useHilfefenster();

  /* Strg und +/-/0 – der Menü-Beschleuniger allein trägt nicht, siehe dort. */
  useZoomtasten(einstellungen.werte.anzeigegroesse);

  /* Der Textabstand hängt an der Anwendung und nicht an der Einstellungskarte.
     Bis 0.24.1 setzte ihn `Textabstandswahl` selbst – also nur, solange der
     Startbildschirm im Baum stand. Profilfrage und Zuschnittfrage kommen
     davor und lagen deshalb immer in der Werksdarstellung. Solange die Datei
     noch nicht gelesen ist, gilt der Zwischenspeicher; siehe dort. */
  useTextabstand(einstellungen.laedt ? undefined : einstellungen.werte.textabstand);

  /* Alle drei Werte stehen im Vertrag als optional; hier werden sie zu
     einer Entscheidung aufgelöst, damit unten nirgends mehr ein
     Rückfallwert steht. Die beiden Schwierigkeitsregler fallen auf „aus"
     zurück – dieselbe Aussage wie in EINSTELLUNGEN_STANDARD. */
  const optionenMischen = einstellungen.werte.optionenMischen ?? false;
  const antwortzahlVerbergen = einstellungen.werte.antwortzahlVerbergen ?? false;
  const vorlesen = einstellungen.werte.vorlesen ?? false;
  /* Nur zusammen wirksam: Wer die Sprachausgabe abschaltet, schaltet damit
     auch die selbsttätige Ansage ab, ohne den zweiten Schalter zu suchen. */
  const vorlesenAutomatisch = vorlesen && (einstellungen.werte.vorlesenAutomatisch ?? false);
  const vorlesenErklaerung = vorlesen && (einstellungen.werte.vorlesenErklaerungBeiFehler ?? false);
  /* Bereinigt statt roh durchgereicht: Ein Wert, den es nicht als Stufe gibt,
     ginge sonst als `rate` an die Sprachausgabe – dort ist alles zwischen
     0,1 und 10 erlaubt, und eine von Hand in die Datei geschriebene 9 machte
     die Funktion unbrauchbar. */
  const sprechtempo = sprechtempoBereinigen(einstellungen.werte.sprechtempo);

  const [ansicht, setzeAnsicht] = useState<Ansicht>({ art: 'start' });

  /*
    Von welcher Karte des Startbildschirms aus die Ansicht verlassen wurde.

    ## Warum es das gibt

    Bis Fassung 0.25.0 endete **jeder** Rückweg am Seitenanfang. Neun Rückwege
    laufen über `zumStart`, und der Fokuseffekt darunter setzte den Fokus
    danach ausnahmslos auf `einstiegRef` – beim Startbildschirm also auf
    „Heute lernen“ ganz oben. Wer bis „Über diese Software“ gerollt war
    (nachgemessen bei rund 7900 von 8530 Bildpunkten) und zurückkam, stand
    wieder oben und musste den ganzen Weg noch einmal rollen. Mit der Tastatur
    waren es von dort bis zum letzten Weg in eine andere Ansicht
    einundvierzig Tabulatorhalte.

    ## Warum der Fokus und nicht die Bildlaufstelle

    Es wurde nie eine Bildlaufstelle falsch wiederhergestellt – es wurde
    keine gemerkt. Der Sprung nach oben ist die **Nebenwirkung** von
    `focus()`: Ein Fokussprung rollt sein Ziel ins Bild, sofern nicht
    `preventScroll` gesetzt ist. Die Aufgabe lautet deshalb nicht „Stelle
    merken“, sondern „Fokusziel richtungsabhängig machen“.

    Eine gemerkte Zahl (`window.scrollY`) wäre zudem falsch: Sie gilt nur für
    die Anzeigegröße, den Textabstand und den Seiteninhalt, bei denen sie
    erhoben wurde. Ein Anker im Dokument gilt immer.

    ## Warum die Kartenüberschrift und nicht der Knopf

    Der Knopf wäre das naheliegende Ziel und ist gemessen falsch: Bei
    320 CSS-Pixeln steht er bis zu 406 Bildpunkte unterhalb des Sichtfelds,
    wenn die Karte oben anliegt. Ein Fokusring außerhalb des Bildes ist kein
    sichtbarer Fokusring (WCAG 2.4.7). Die Überschrift ist zugleich der
    Rollanker und die Ansage – ein Bildschirmleser nennt eine Ortsangabe
    statt einer Bestätigung.

    ## Warum ein `ref` und nicht der Ansichtszustand

    Aus demselben Grund wie beim Suchtext darüber: Der Wert wird
    ausschließlich im Fokuseffekt gelesen; ein Neuaufbau wegen seiner
    Änderung wäre unnötig. Und er beschreibt die Ansicht, die **verlassen**
    wird, nicht die betretene – im Zielobjekt müsste ihn jeder
    `setzeAnsicht`-Aufruf mitschleppen.
  */
  const letzterAufbruch = useRef<string | null>(null);

  /*
    „Wer lernt?" – ein Tor vor dem Startbildschirm, aber nur, wenn es
    tatsächlich etwas zu entscheiden gibt.

    Die Entscheidung fällt einmal je Programmstart und erst, wenn die
    Profilliste vorliegt: Vorher stünde eine Weile der Startbildschirm da,
    über den die Frage dann nachträglich hereinspringt. Solange „wartet"
    gilt, zeigt der Bereich deshalb nur, dass geladen wird – bei einer
    lokalen Datenbank sind das Millisekunden.

    Danach bleibt es bei „frei". Legt jemand in den Einstellungen ein
    zweites Profil an, darf ihm nicht mitten im Tun die Startfrage vor die
    Nase springen; gewechselt wird dort über die Liste.
  */
  const [profiltor, setzeProfiltor] = useState<'wartet' | 'frage' | 'zuschnitt' | 'frei'>('wartet');

  /*
    Beim Ansichtswechsel wandert der Fokus auf die Einstiegsüberschrift der
    neuen Ansicht – sonst fiele er auf `<body>` zurück, sobald die auslösende
    Schaltfläche verschwindet. Beim allerersten Aufbau geschieht das bewusst
    nicht: dort soll der Skip-Link der erste Tabstopp bleiben.

    Das Profiltor zählt dabei wie ein Ansichtswechsel: Wer die Frage „Wer
    lernt?" beantwortet hat, soll auf der Überschrift des Startbildschirms
    landen und nicht wieder ganz oben.
  */
  const einstiegRef = useRef<HTMLHeadingElement | null>(null);
  const ersterAufbau = useRef(true);

  useEffect(() => {
    if (ersterAufbau.current) {
      /* Der Aufbau gilt erst als abgeschlossen, wenn feststeht, was gezeigt
         wird. Sonst zöge der Wechsel von „wartet" auf „frei" den Fokus vom
         Sprunglink weg – gleich beim Start, ohne dass jemand etwas getan
         hätte (WCAG 3.2.1). */
      if (profiltor !== 'wartet') {
        ersterAufbau.current = false;
      }
      return;
    }

    /* Der Rückweg auf den Startbildschirm endet dort, wo er begann. Das
       Muster ist dasselbe, das `Sprungmenue.tsx`, `Glossaransicht.tsx` und
       `Hilfedialog.tsx` benutzen: erst rollen, dann ohne zweites Rollen
       fokussieren. `preventScroll` allein genügte nicht – der Bildlauf
       bliebe dann auf dem Zahlenwert der verlassenen Ansicht stehen. */
    /* Geleert wird erst auf dem Rückweg, nicht schon hier: Dieser Effekt
       läuft auch beim Hinweg, und ein unbedingtes Leeren nähme dem Rückweg
       den Wert, bevor er ihn lesen kann. Die übrigen Leerstellen stehen
       dort, wo ein neuer Vorgang beginnt, und es sind **drei**:
       `sitzungStarten`, `ausSucheUeben` und `gehe` im Menü. Diese
       Aufzählung nannte bis Fassung 0.26.0 nur die ersten beiden — bei
       einem einzigen Merkwert war die Buchführung schon ausgefallen. Wer
       einen zweiten anlegen will, sollte das lesen: Der bessere Weg ist,
       einen Wert mit seinem Vorgang reisen zu lassen statt neben ihm
       (siehe `Suchanfrage` in `lernen/typen.ts`). */
    if (ansicht.art === 'start' && letzterAufbruch.current !== null) {
      const ziel = letzterAufbruch.current;
      letzterAufbruch.current = null;
      const knoten = document.getElementById(ziel);
      if (knoten !== null) {
        if (typeof knoten.scrollIntoView === 'function') {
          knoten.scrollIntoView({ block: 'start' });
        }
        knoten.focus({ preventScroll: true });
        return;
      }
      /* Karte nicht gefunden – dann lieber die Einstiegsüberschrift als
         `<body>`. Der Fall ist erreichbar: Ein Block kann verschwunden sein,
         etwa nach einem Profilwechsel. */
    }

    einstiegRef.current?.focus();
  }, [ansicht, profiltor]);

  const sitzungStarten = useCallback(
    (titel: string, quelle: Sitzungsquelle) => {
      /* Die Einstellung gehört zum Nutzer, nicht zum einzelnen Aufruf – sie
         hier anzuhängen deckt alle sechs Wege mit fester Fragenliste auf
         einmal. Bei einer Sitzung nach Filter trägt sie der Filter, den die
         Aufrufer selbst bauen. */
      const mitEinstellung: Sitzungsquelle =
        quelle.art === 'fragen' ? { ...quelle, optionenMischen } : quelle;
      /* Eine Sitzung ist ein neuer Vorgang: Der Merkwert einer zuvor
         verlassenen Karte gilt danach nicht mehr. Sonst führte „Zum Start“
         nach dem Üben hartnäckiger Fragen an die Suchkarte, nur weil vorher
         einmal die Suche offen war. */
      letzterAufbruch.current = null;
      setzeAnsicht({ art: 'sitzung', auftrag: { id: Date.now(), titel, quelle: mitEinstellung } });
    },
    [optionenMischen],
  );

  const mitFilter = useCallback(
    (titel: string, filter: SitzungsFilter) => {
      sitzungStarten(titel, { art: 'filter', filter });
    },
    [sitzungStarten],
  );

  const zumStart = useCallback(() => {
    setzeAnsicht({ art: 'start' });
  }, []);

  /**
   * Eine Ansicht vom Startbildschirm aus betreten und die Karte merken.
   *
   * Nur von dort: Wer aus der Prüfungsauswertung heraus eine neue Simulation
   * beginnt, kommt später nicht von der Karte, und ein Merkwert von dort
   * setzte ihn an einer Stelle ab, an der er nie war.
   */
  const vonKarte = useCallback((kennung: string, ziel: Ansicht) => {
    letzterAufbruch.current = kennung;
    setzeAnsicht(ziel);
  }, []);

  const zurSuche = useCallback((anfrage?: Suchanfrage) => {
    setzeAnsicht({ art: 'suche', anfrage });
  }, []);

  /** Aus der Trefferliste heraus üben – über die vorhandene Sitzungsquelle. */
  const ausSucheUeben = useCallback(
    (frageIds: readonly string[], titel: string, anfrage: Suchanfrage) => {
      /* Der Merkwert gilt nur für den unmittelbaren Rückweg. Wer aus der
         Trefferliste heraus übt, bekommt am Sitzungsende „Zurück zur Suche“
         und „Zum Start“ nebeneinander; führte „Zum Start“ dann an die
         Suchkarte, wären es zwei verschieden benannte Wege zu fast demselben
         Ort. Das Sprungmenü sagt dazu: „Sie muss das Ziel vorhersagen.“ */
      letzterAufbruch.current = null;
      setzeAnsicht({
        art: 'sitzung',
        auftrag: {
          id: Date.now(),
          titel,
          quelle: { art: 'fragen', frageIds: [...frageIds] },
          ausSuche: anfrage,
        },
      });
    },
    [],
  );

  const sitzungBeenden = useCallback(
    (bilanz: SitzungsBilanz) => {
      setzeAnsicht({ art: 'auswertung', bilanz });
      // Übersicht und Plan spiegeln jetzt einen neuen Stand wider.
      lernstand.neuLaden();
      lernplan.neuLaden();
    },
    [lernstand, lernplan],
  );

  const fehlerWiederholen = useCallback(
    (bilanz: SitzungsBilanz) => {
      sitzungStarten('Fehler wiederholen', {
        art: 'fragen',
        frageIds: bilanz.falscheFragen.map((eintrag) => eintrag.frageId),
      });
    },
    [sitzungStarten],
  );

  const pruefungWaehlen = useCallback(() => {
    setzeAnsicht({ art: 'pruefungswahl' });
  }, []);

  /*
    Einen unterbrochenen Lauf fortsetzen.

    Die Karte hat vorher gefragt; ab hier läuft die Uhr. `vergessen` statt
    `verwerfen`: Der Bogen bleibt in der Datenbank – er wird gerade
    weitergeführt und soll auch eine zweite Unterbrechung überstehen.
  */
  const pruefungFortsetzen = useCallback(() => {
    const lauf = offenePruefung.lauf;
    if (lauf === null) {
      return;
    }
    offenePruefung.vergessen();
    setzeAnsicht({
      art: 'pruefung',
      lauf: { id: Date.now(), auftrag: lauf.auftrag, fortsetzung: lauf },
    });
  }, [offenePruefung]);

  const pruefungStarten = useCallback(
    (auftrag: Pruefungsauftrag) => {
      /* Die Einstellung gehört zum Nutzer, nicht zum Prüfungsprofil –
         deshalb wird sie hier ergänzt und nicht in der Prüfungswahl
         abgefragt. */
      setzeAnsicht({
        art: 'pruefung',
        lauf: { id: Date.now(), auftrag: { ...auftrag, optionenMischen } },
      });
    },
    [optionenMischen],
  );

  const pruefungBeenden = useCallback(
    (abschluss: Pruefungsabschluss) => {
      setzeAnsicht({ art: 'pruefungsergebnis', abschluss });
      // Ein gespeicherter Lauf verändert den Lernstand.
      lernstand.neuLaden();
      lernplan.neuLaden();
      /* Der Kern hat die offene Zeile in derselben Transaktion gelöscht;
         hier wird nur nachgesehen, damit die Karte verschwindet. */
      offenePruefung.neuLaden();
    },
    [lernstand, lernplan, offenePruefung],
  );

  /*
    Einen laufenden Bogen abbrechen – „Abbrechen und verwerfen“ und „Ohne
    Auswertung verwerfen“ führen beide hierher.

    Bis 0.24.1 setzte dieser Weg nur die Ansicht zurück. Die gesicherte Zeile
    blieb dabei in `pruefung_offen` stehen, und beim nächsten Start stand der
    ausdrücklich verworfene Lauf wieder als „Unterbrochene Simulation“ da –
    eine Schaltfläche mit dem Wort „verwerfen“, die nichts verwarf.

    Die Ansicht wechselt sofort und wartet nicht auf den Kanal: Der Abbruch
    ist eine Entscheidung des Prüflings und darf nicht an einer Zusage aus dem
    Hauptprozess hängen. Scheitert das Verwerfen, bleibt die Karte beim
    nächsten Start stehen – unschön, aber ehrlich, denn dann ist der Bogen
    wirklich noch da.
  */
  const pruefungVerwerfen = useCallback(() => {
    setzeAnsicht({ art: 'start' });
    void offenePruefung.verwerfen().catch((fehler: unknown) => {
      console.warn('[pruefung] Der abgebrochene Lauf ließ sich nicht verwerfen:', fehler);
    });
  }, [offenePruefung]);

  const pruefungsfehlerWiederholen = useCallback(
    (frageIds: readonly string[]) => {
      sitzungStarten('Fehler aus der Prüfungssimulation', { art: 'fragen', frageIds });
    },
    [sitzungStarten],
  );

  /* Als feste Fragenliste und nicht über den Filter: Die Liste steht bereits
     auf dem Bildschirm, und wer sie sieht, soll genau sie üben – nicht eine
     zweite Auswahl, die der Kern in derselben Sekunde neu berechnet. */
  const hartnaeckigeUeben = useCallback(
    (frageIds: readonly string[]) => {
      sitzungStarten('Hartnäckige Fragen', { art: 'fragen', frageIds });
    },
    [sitzungStarten],
  );

  /* Am Ende einer Sitzung und nirgends sonst: Eine Sitzung zu starten ersetzt
     die laufende, und zwar ohne Rueckfrage. Mitten im Lernen waere derselbe
     Knopf eine Falle. */
  const verwandteUeben = useCallback(
    (frageIds: readonly string[]) => {
      sitzungStarten('Verwandte Fragen', { art: 'fragen', frageIds });
    },
    [sitzungStarten],
  );

  /*
    Das Menü „Gehe zu“ – die einzige Navigation, die nicht wegrollt.

    ## Warum die Tabelle hier steht und nicht im Hilfehaken

    Sie stand bis Fassung 0.25.0 in `useHilfefenster`, weil es nur einen
    Befehl gab. Der `Record` über den Befehlstyp ist der Grund, warum sie
    umgezogen ist: Er löst einen Übersetzungsfehler aus, sobald ein Befehl
    unbehandelt bleibt. Zwei Tabellen an zwei Orten hätten diesen Zwang
    halbiert – jede wäre für sich vollständig gewesen, und keine hätte
    bemerkt, dass die andere einen Befehl doppelt oder gar nicht behandelt.

    ## Warum ein `ref` für die laufende Ansicht

    Stünde `ansicht` in der Abhängigkeitsliste, meldete sich der Beobachter
    bei jedem Ansichtswechsel neu an und ab. Der Wert wird nur im Augenblick
    eines Menübefehls gelesen; ein `ref` genügt.
  */
  const ansichtRef = useRef(ansicht);
  /* Geschrieben in einem Effekt und nicht beim Zeichnen: Ein Schreibzugriff
     auf ein `ref` während des Aufbaus ist bei React 19 ein Fehler
     (`react-hooks/refs`), weil ein verworfener Aufbau ihn sonst hinterließe. */
  useEffect(() => {
    ansichtRef.current = ansicht;
  }, [ansicht]);

  const [menueansage, setzeMenueansage] = useState('');

  useEffect(() => {
    const beobachten = lernBruecke()?.menuebefehlBeobachten;

    /*
      Aus einer laufenden Sitzung oder Prüfung führt kein Menüeintrag heraus.

      Der Grund steht schon geschrieben, an der Stelle, an der die
      Zurückleiste dort ausgelassen wird: „Dort ist der Ausstieg eine
      Entscheidung mit Folgen (‚Sitzung beenden‘, ‚Prüfung abbrechen‘) und
      steht bei den übrigen Aktionen, nicht als beiläufiger Pfeil am Rand.“
      Ein Menüeintrag ist derselbe beiläufige Weg, nur weiter oben.

      Ausgegraut werden die Einträge nicht: Der Kanal ist einseitig, der
      Hauptprozess erfährt vom Zustand der Oberfläche nichts. Statt
      stillschweigend nichts zu tun, sagt die Anwendung, warum sie nicht
      folgt – über die vorhandene Statusregion, nicht über eine zweite.
    */
    const gehe = (ziel: Ansicht): void => {
      const art = ansichtRef.current.art;

      /*
        Vier Ansichten tragen etwas, das nur sie haben und das kein Weg
        zurückholt. Bei Sitzung und Prüfung ist es die laufende Arbeit; bei
        den beiden Auswertungen ist es ihr Inhalt selbst — `bilanz` und
        `abschluss` stehen im Ansichtsobjekt (`lernen/typen.ts`), und das
        Menü kennt kein Ziel, das sie wiederherstellte.

        Die beiden Auswertungen standen bis 0.26.0 **nicht** in dieser Liste.
        Das war ein Fehler dieser Fassung: „Gehe zu → Glossar“ aus einer
        Prüfungsauswertung heraus verwarf die Fehlerliste, die Themenanalyse
        und den Knopf „Fehler wiederholen“ ohne Rückfrage. Der Lauf selbst
        bleibt im Verlauf erhalten — die Auswertung dazu nicht.

        Ausgegraut werden die Einträge nicht: Der Kanal ist einseitig, der
        Hauptprozess erfährt vom Zustand der Oberfläche nichts.
      */
      const gesperrt: Readonly<Record<string, string>> = {
        sitzung: 'Beenden Sie zuerst die Lernsitzung. Der Weg dorthin steht unter den Antworten.',
        pruefung:
          'Beenden Sie zuerst die Prüfungssimulation. Abgeben und Abbrechen stehen im Bogen.',
        auswertung:
          'Diese Auswertung lässt sich nicht wieder aufrufen. ' +
          'Die Wege von hier stehen unter der Bilanz.',
        pruefungsergebnis:
          'Diese Auswertung lässt sich nicht wieder aufrufen. ' +
          'Die Wege von hier stehen unter dem Ergebnis; der Lauf selbst bleibt im Verlauf.',
      };

      const grund = gesperrt[art];
      if (grund !== undefined) {
        setzeMenueansage(grund);
        return;
      }
      setzeMenueansage('');
      /* Ein Menübefehl kommt von keiner Karte – ein stehengebliebener
         Merkwert setzte den Rückweg sonst an einer Stelle ab, an der der
         Nutzende nie war. */
      letzterAufbruch.current = null;
      setzeAnsicht(ziel);
    };

    const wirkung: Record<Menuebefehl, () => void> = {
      hilfe: () => {
        hilfe.oeffnen();
      },
      'gehe-zu-start': () => {
        gehe({ art: 'start' });
      },
      'gehe-zu-kapitelwahl': () => {
        gehe({ art: 'kapitelwahl' });
      },
      'gehe-zu-pruefungswahl': () => {
        gehe({ art: 'pruefungswahl' });
      },
      'gehe-zu-suche': () => {
        gehe({ art: 'suche' });
      },
      'gehe-zu-glossar': () => {
        gehe({ art: 'glossar' });
      },
      'gehe-zu-ueber': () => {
        gehe({ art: 'ueber' });
      },
    };

    return beobachten?.((befehl) => {
      wirkung[befehl]();
    });
  }, [hilfe]);

  const katalogIndex = katalogZustand.art === 'fertig' ? katalogZustand.index : null;

  /*
    Der Kapitelausschluss des gewählten Lernprofils.

    Er gilt profilweit – die Erststart-Frage sagt zu, dass abgewählte Fragen
    „in keiner Sitzung und in keiner Zahl mehr“ vorkommen. Kapitelwahl und
    Prüfungssimulation brauchen ihn deshalb beide; ihn zweimal aus der
    Profilliste zu suchen wäre die nächste Zahl, die auseinanderläuft.
  */
  const kapitelAusschluss =
    lernstand.profile.find((eintrag) => eintrag.id === lernstand.profilId)?.kapitelAusschluss ?? [];

  /*
    Die Frage nach dem abwählbaren Kapitel – einmal, beim allerersten Start.

    Drei Bedingungen, und jede hat einen Grund. Sie wurde noch nie gestellt
    (Einstellung, nicht Lernstand). Der Katalog ist geladen, sonst gäbe es
    keine Zahl zu nennen. Und es ist noch keine Frage beantwortet: Wer schon
    gelernt hat, bekommt sie nicht mehr vorgesetzt – für ihn ist es kein
    erster Start, und der Schalter steht ohnehin unter „Ihr Lernplan“.
  */
  const zuschnittOffen =
    einstellungen.werte.zuschnittGefragt !== true &&
    katalogIndex !== null &&
    lernstand.uebersicht?.beantwortet === 0;

  /*
    Erst entscheiden, wenn feststeht, wonach zu entscheiden ist.

    Solange der Katalog noch lädt, ist `zuschnittOffen` zwangsläufig falsch –
    ohne ihn gäbe es keine Zahl zu nennen. Würde hier schon geschaltet, fiele
    das Tor auf „frei“, und die Frage käme nie. Deshalb wartet es auf den
    Katalog, solange dieser überhaupt noch kommen kann.
  */
  const katalogKommtNoch = katalogZustand.art === 'laedt';

  if (profiltor === 'wartet' && !lernstand.laedt && !katalogKommtNoch) {
    setzeProfiltor(lernstand.profile.length > 1 ? 'frage' : zuschnittOffen ? 'zuschnitt' : 'frei');
  }

  /* Nach der Wahl merken, dass gefragt wurde – und erst dann weiterschalten.
     Andernfalls käme die Frage beim nächsten Start wieder. */
  const zuschnittBeantworten = (mitKapitel: boolean): void => {
    einstellungen.aendern({ zuschnittGefragt: true });
    if (mitKapitel) {
      setzeProfiltor('frei');
      return;
    }
    void lernplan.kapitelAusschlussSetzen([KAPITEL_IV]).then(() => {
      lernstand.neuLaden();
      setzeProfiltor('frei');
    });
  };

  /*
    In einer Sitzung oder Prüfung entfällt der Seitenkopf.

    Nachgemessen: Kopf und Fuß kosten zusammen 324 Pixel von 1063 sichtbaren
    – fast ein Drittel des Bildschirms für eine Begrüßung, die man beim
    Beantworten der zwölften Frage nicht mehr liest.

    Auch die Überschrift gehört dort nicht hin. Der Name der Anwendung steht
    in der Titelleiste des Fensters – ihn im Inhalt zu wiederholen ist
    Denken aus dem Web. Die oberste Überschrift jeder Ansicht benennt
    stattdessen, was diese Ansicht zeigt: die Frage selbst im Lernmodus, den
    Lauf in der Prüfung. Wer per Überschrift navigiert, landet damit dort,
    wo es um etwas geht.

    Die Quellenangabe im Fuß bleibt in jedem Fall stehen: Sie ist keine
    Zierde, sondern Pflicht.
  */
  /*
    Der Seitenkopf steht nur noch auf dem Startbildschirm.

    Jede Ansicht führt ihre eigene h1 und benennt damit, worum es dort geht:
    „Glossar“, „Über diese Software“, die Frage selbst. Der Name der
    Anwendung steht in der Titelleiste des Fensters – als Inhaltsüberschrift
    wäre er in jeder Ansicht dieselbe, nichtssagende Antwort auf die Frage
    „wo bin ich?“.
  */
  const knapp = ansicht.art !== 'start';

  /*
    Der Rueckweg gehoert nach oben.

    Er stand in allen sechs Nebenansichten am Ende der Seite – im Glossar
    hinter 118 Eintraegen, in der Pruefungsauswertung hinter der ganzen
    Themenanalyse. Wer sich verlaufen hatte, musste erst durch den gesamten
    Inhalt scrollen, um wieder herauszufinden.

    Nicht in Sitzung und Pruefung: Dort ist der Ausstieg eine Entscheidung
    mit Folgen („Sitzung beenden“, „Pruefung abbrechen“) und steht bei den
    uebrigen Aktionen, nicht als beilaeufiger Pfeil am Rand.
  */
  const mitRueckweg = (
    [
      'kapitelwahl',
      'auswertung',
      'pruefungswahl',
      'pruefungsergebnis',
      'glossar',
      'suche',
      'ueber',
    ] as const
  ).some((art) => art === ansicht.art);

  return (
    <>
      {/* Erster fokussierbarer Knoten im Dokument (WCAG 2.4.1). */}
      <a className="skip-link" href="#inhalt">
        Direkt zum Hauptinhalt springen
      </a>

      <div className="seite">
        {knapp ? null : (
          <header className="kopfbereich">
            <p className="kopfbereich__augenbraue">Prüfungsvorbereitung</p>
            <h1>Waffensachkunde – Lernsoftware</h1>
            <p className="einleitung">
              Ruhig lernen, in Ihrem Tempo. Diese Anwendung läuft vollständig offline auf Ihrem
              Rechner – es werden keine Daten übertragen.
            </p>
          </header>
        )}

        {/* tabIndex={-1}, damit der Skip-Link den Fokus tatsächlich hierher bewegt. */}
        <main id="inhalt" tabIndex={-1} className="abschnitt">
          {mitRueckweg ? <Zurueckleiste beschriftung="Zum Start" onZurueck={zumStart} /> : null}

          {ansicht.art === 'start' && profiltor === 'wartet' ? (
            <p>Ihre Lernprofile werden geladen …</p>
          ) : null}

          {ansicht.art === 'start' && profiltor === 'frage' ? (
            <Profilfrage
              profile={lernstand.profile}
              vorausgewaehlt={lernstand.profilId}
              einstiegRef={einstiegRef}
              onWeiter={(gewaehlt) => {
                lernstand.wechselnZu(gewaehlt);
                setzeProfiltor(zuschnittOffen ? 'zuschnitt' : 'frei');
              }}
            />
          ) : null}

          {ansicht.art === 'start' && profiltor === 'zuschnitt' && katalogIndex !== null ? (
            <Zuschnittfrage
              kapitelTitel={katalogIndex.bereichTitel(KAPITEL_IV)}
              fragen={
                katalogIndex.katalog.fragen.filter((frage) => frage.kapitel === KAPITEL_IV).length
              }
              fragenGesamt={katalogIndex.katalog.fragen.length}
              einstiegRef={einstiegRef}
              onWeiter={zuschnittBeantworten}
            />
          ) : null}

          {ansicht.art === 'start' && profiltor === 'frei' ? (
            <Startbildschirm
              einstiegRef={einstiegRef}
              lernstand={lernstand}
              lernplan={lernplan}
              katalogZustand={katalogZustand}
              katalogIndex={katalogIndex}
              onKatalogNeuLaden={katalogNeuLaden}
              kuerzel={kuerzel}
              einstellungen={einstellungen}
              hilfsmittelAktiv={hilfsmittelAktiv}
              themaAuswahl={auswahl}
              onThema={auswahlSetzen}
              onWeiterlernen={() => {
                mitFilter('Weiterlernen', {
                  anzahl: SITZUNGSUMFANG,
                  mischen: true,
                  optionenMischen,
                });
              }}
              hartnaeckigeFragen={hartnaeckigeFragen}
              onHartnaeckigeUeben={hartnaeckigeUeben}
              onKapitelWahl={() => {
                setzeAnsicht({ art: 'kapitelwahl' });
              }}
              onNurFehler={() => {
                mitFilter('Nur Fehler', {
                  nurFehler: true,
                  anzahl: SITZUNGSUMFANG,
                  optionenMischen,
                });
              }}
              onGemerkte={() => {
                mitFilter('Gemerkte Fragen', {
                  nurGemerkte: true,
                  anzahl: SITZUNGSUMFANG,
                  optionenMischen,
                });
              }}
              onOffeneFragen={() => {
                mitFilter('Offene Fragen', {
                  nurOffene: true,
                  anzahl: SITZUNGSUMFANG,
                  mischen: true,
                  optionenMischen,
                });
              }}
              offenePruefung={offenePruefung}
              onPruefungFortsetzen={pruefungFortsetzen}
              /* Die vier Wege, die vom Startbildschirm aus tief unten
                 beginnen können – sie merken ihre Karte, damit der Rückweg
                 dort wieder ankommt. „Kapitel wählen“ und „Weiterlernen“
                 brauchen das nicht: Ihre Knöpfe stehen in der Einstieg-Karte
                 im ersten Bildschirm (`e2e/blickfeld.spec.ts`). */
              onPruefung={() => {
                vonKarte('pruefungsteil-titel', { art: 'pruefungswahl' });
              }}
              onGlossar={() => {
                vonKarte('glossar-titel', { art: 'glossar' });
              }}
              onSuche={() => {
                vonKarte('suche-titel', { art: 'suche' });
              }}
              onUeber={() => {
                vonKarte('ueber-start-titel', { art: 'ueber' });
              }}
            />
          ) : null}

          {ansicht.art === 'kapitelwahl' && katalogIndex !== null ? (
            <KapitelWahl
              kapitel={katalogIndex.katalog.kapitel}
              abgewaehlt={kapitelAusschluss}
              einstiegRef={einstiegRef}
              onKapitel={(kapitelId, titel) => {
                mitFilter(`Kapitel ${kapitelId} – ${titel}`, {
                  kapitel: [kapitelId],
                  anzahl: SITZUNGSUMFANG,
                  mischen: true,
                  optionenMischen,
                });
              }}
              onAbschnitt={(abschnittId, titel) => {
                mitFilter(`${abschnittId} – ${titel}`, {
                  abschnitte: [abschnittId],
                  anzahl: SITZUNGSUMFANG,
                  mischen: true,
                  optionenMischen,
                });
              }}
              /* Als feste Fragenliste: Die Themengruppen kennt der
                 Anwendungskern nicht - sie sind eine Ergaenzung dieser
                 Software und stehen in content/themen.json. */
              onThema={(frageIds, titel) => {
                sitzungStarten(`Thema: ${titel}`, { art: 'fragen', frageIds });
              }}
              onZurueck={zumStart}
            />
          ) : null}

          {ansicht.art === 'sitzung' && katalogIndex !== null ? (
            <Lernsitzung
              key={ansicht.auftrag.id}
              auftrag={ansicht.auftrag}
              katalogIndex={katalogIndex}
              profilId={lernstand.profilId}
              kuerzelAktiv={kuerzel.aktiv}
              vorlesenAktiv={vorlesen}
              vorlesenAutomatisch={vorlesenAutomatisch}
              vorlesenErklaerung={vorlesenErklaerung}
              sprechtempo={sprechtempo}
              antwortzahlVerbergen={antwortzahlVerbergen}
              einstiegRef={einstiegRef}
              onBeenden={sitzungBeenden}
              onZumStart={zumStart}
            />
          ) : null}

          {ansicht.art === 'auswertung' ? (
            <Sitzungsende
              bilanz={ansicht.bilanz}
              einstiegRef={einstiegRef}
              onWiederholen={() => {
                fehlerWiederholen(ansicht.bilanz);
              }}
              onVerwandteUeben={verwandteUeben}
              /* Nur nach einer gewöhnlichen Sitzung: Wer aus der Suche kam
                 oder gerade Fehler wiederholt hat, will nicht „weiter“,
                 sondern zurück zu seinem Ausgangspunkt. */
              onWeiterlernen={
                ansicht.bilanz.ausSuche === undefined
                  ? () => {
                      mitFilter('Weiterlernen', {
                        anzahl: SITZUNGSUMFANG,
                        mischen: true,
                        optionenMischen,
                      });
                    }
                  : undefined
              }
              onZurueckZurSuche={
                ansicht.bilanz.ausSuche === undefined
                  ? undefined
                  : () => {
                      zurSuche(ansicht.bilanz.ausSuche);
                    }
              }
              onZumStart={zumStart}
            />
          ) : null}

          {ansicht.art === 'pruefungswahl' ? (
            <Pruefungswahl
              einstiegRef={einstiegRef}
              kapitelIVTitel={katalogIndex === null ? null : katalogIndex.bereichTitel(KAPITEL_IV)}
              kapitelAusschluss={kapitelAusschluss}
              katalogIndex={katalogIndex}
              onStarten={pruefungStarten}
              onZurueck={zumStart}
            />
          ) : null}

          {ansicht.art === 'pruefung' && katalogIndex !== null ? (
            <Pruefungslauf
              key={ansicht.lauf.id}
              lauf={ansicht.lauf}
              katalogIndex={katalogIndex}
              profilId={lernstand.profilId}
              vorlesenAktiv={vorlesen}
              einstiegRef={einstiegRef}
              onFertig={pruefungBeenden}
              onAbbrechen={pruefungVerwerfen}
            />
          ) : null}

          {ansicht.art === 'pruefungsergebnis' ? (
            <Pruefungsauswertung
              abschluss={ansicht.abschluss}
              katalogIndex={katalogIndex}
              profilId={lernstand.profilId}
              einstiegRef={einstiegRef}
              vorlesenAktiv={vorlesen}
              sprechtempo={sprechtempo}
              onFehlerWiederholen={pruefungsfehlerWiederholen}
              onNeueSimulation={pruefungWaehlen}
              onZumStart={zumStart}
            />
          ) : null}

          {ansicht.art === 'suche' && katalogIndex !== null ? (
            <Suchansicht
              einstiegRef={einstiegRef}
              katalogIndex={katalogIndex}
              anfangsanfrage={ansicht.anfrage}
              onUeben={ausSucheUeben}
              onZurueck={zumStart}
            />
          ) : null}

          {ansicht.art === 'glossar' ? (
            <Glossaransicht
              einstiegRef={einstiegRef}
              glossar={glossar.glossar}
              laedt={glossar.laedt}
              vorlesenAktiv={vorlesen}
              sprechtempo={sprechtempo}
              onZurueck={zumStart}
            />
          ) : null}

          {ansicht.art === 'ueber' ? (
            <UeberSoftware
              einstiegRef={einstiegRef}
              katalogIndex={katalogIndex}
              onZurueck={zumStart}
            />
          ) : null}
        </main>

        <footer className={`fussbereich${knapp ? ' fussbereich--knapp' : ''}`}>
          {/*
            Der Weg zur Hilfe – in jeder Ansicht, an derselben Stelle, immer
            als erstes im Fußbereich. Genau das verlangt WCAG 3.2.6:
            konsistente Hilfe in gleicher relativer Reihenfolge.

            Der Fußbereich ist die einzige Stelle, die es in allen zehn
            Ansichten gibt; der Rückweg oben fehlt in Sitzung und Prüfung.
            Und weil die Hilfe ein Dialog ist und keine Ansicht, kostet sie
            hier nichts: Die Sitzung bleibt stehen, der Prüfungsbogen auch.
          */}
          <button
            type="button"
            className="knopf knopf--leise fussbereich__hilfe"
            aria-haspopup="dialog"
            aria-keyshortcuts="F1"
            onClick={hilfe.oeffnen}
          >
            Hilfe
          </button>

          {/* Der Tastaturhinweis gehört auf den Startbildschirm; in der Sitzung
              steht er ohnehin unter „Tastenkürzel dieser Ansicht“. */}
          {knapp ? null : (
            <p>
              Bedienung vollständig per Tastatur möglich: mit <kbd>Tab</kbd> weiter, mit den{' '}
              <kbd>Pfeiltasten</kbd> innerhalb von Gruppen.
            </p>
          )}
          {katalogIndex !== null ? (
            <p className="fussbereich__quelle">{katalogIndex.katalog.meta.quellenangabe}</p>
          ) : null}
        </footer>
      </div>

      {/* Außerhalb von `.seite`: Ein modaler Dialog liegt in der obersten
          Ebene des Browsers und gehört auch im Baum nicht in den Inhalt,
          über den er sich legt. */}
      {/* Kontextsensitiv: Die Hilfe schlägt beim Kapitel zur gerade
          sichtbaren Ansicht auf. Ansichten ohne Zuordnung – der
          Startbildschirm etwa – beginnen weiterhin oben, beim
          Inhaltsverzeichnis. */}
      <Hilfedialog
        offen={hilfe.offen}
        anfangskapitel={HILFE_ZUR_ANSICHT[ansicht.art]}
        onSchliessen={hilfe.schliessen}
      />

      {/*
        Statusmeldungen für Screenreader. Die Region ist von Anfang an im DOM
        vorhanden (sonst würde die erste Meldung verschluckt) und höflich –
        sie unterbricht die aktuelle Vorlesung nicht.
      */}
      <div className="nur-screenreader" role="status" aria-live="polite" aria-atomic="true">
        {/* Eine Region, zwei Quellen. Eine zweite Region wäre gegen die
            Auflage des Prüfplans, sie sparsam einzusetzen; die Meldung des
            Menüs ist die dringendere, weil sie eine Handlung erklärt, die
            gerade nicht ausgeführt wurde. */}
        {menueansage === '' ? ansage : menueansage}
      </div>
    </>
  );
}
