/**
 * Der Lernbericht – das erste druckbare Dokument.
 *
 * **Warum dieses zuerst.** PLAN.md nennt unter D8 vier Dokumente:
 * Fragenlisten, Fehlerprotokoll, Statistik, Lernkarten. Der Bericht über den
 * eigenen Stand ist das einzige davon, das ohne neuen Datenkanal und ohne
 * Änderung am Datenbankschema auskommt – Übersicht, Lernplan und
 * Prüfungsverlauf liegen bereits vor. Er ist außerdem kurz genug, dass sich
 * das erzeugte PDF von Hand mit einem Prüfwerkzeug gegenlesen lässt; bei
 * einer Fragenliste von rund 300 Seiten wäre das keinem mehr zuzumuten.
 *
 * Und er enthält fast keinen amtlichen Wortlaut – nur die Kapitel- und
 * Abschnittstitel. Die Werkzeugkette samt Quellenangabe wird also einmal
 * vollständig gebaut und geprüft, bevor das erste Dokument entsteht, das den
 * Katalog wirklich wiedergibt.
 */

import type { Lernplan, Machbarkeit } from '../lernplan';
import type { Lernuebersicht } from '../lernstand';
import { reifesatz, STUFE_WORT } from '../reife';
import type { Pruefungsverlauf } from '../pruefung';
import { URTEIL_BEZEICHNUNG } from '../pruefung';
import {
  abschnitt,
  absatz,
  dokumentBauen,
  tabelle,
  type Quellenangabe,
  type Zelle,
} from './dokument';
import type { Schriftgroesse } from './stil';

/** Anteil als ganze Prozent – wie in der Oberfläche. */
function prozent(anteil: number): string {
  return `${String(Math.round(anteil * 100))} %`;
}

/**
 * Bearbeitungsdauer in vollen Minuten.
 *
 * Gröber als am Bildschirm mit Absicht: Auf dem Papier steht die Zahl zum
 * Vergleich mehrerer Läufe untereinander, und dafür sind Sekunden Rauschen.
 * Aufgerundet auf mindestens eine Minute – „0 Minuten“ für einen Lauf, den es
 * gab, wäre keine Auskunft.
 */
function dauerInMinuten(millisekunden: number): string {
  const gerundet = Math.max(0, Math.round(millisekunden / 60_000));
  const minuten = gerundet === 0 && millisekunden > 0 ? 1 : gerundet;
  return `${String(minuten)} ${minuten === 1 ? 'Minute' : 'Minuten'}`;
}

/** ISO-Datum als deutsches Datum; Unlesbares bleibt stehen. */
export function deutschesDatum(iso: string): string {
  const zeit = Date.parse(iso);
  if (Number.isNaN(zeit)) {
    return iso;
  }
  return new Date(zeit).toLocaleDateString('de-DE', {
    day: '2-digit',
    month: '2-digit',
    year: 'numeric',
  });
}

const MACHBARKEIT_SATZ: Readonly<Record<Machbarkeit, string>> = Object.freeze({
  kein_termin: 'Es ist kein Prüfungstermin eingetragen.',
  termin_vorbei: 'Der eingetragene Prüfungstermin liegt in der Vergangenheit.',
  entspannt: 'Bis zum Termin bleibt reichlich Zeit.',
  machbar: 'Das Pensum ist bis zum Termin gut zu schaffen.',
  knapp: 'Bis zum Termin wird es knapp.',
  zu_wenig_zeit: 'Bis zum Termin reicht die Zeit für das nötige Pensum nicht aus.',
});

export interface Lernberichtsdaten {
  readonly uebersicht: Lernuebersicht;
  /** `null`, wenn der Lernplan nicht geladen werden konnte. */
  readonly plan: Lernplan | null;
  readonly verlauf: readonly Pruefungsverlauf[];
  readonly profilName: string;
  readonly quelle: Quellenangabe;
  /** Zeitpunkt der Erstellung als ISO-Zeichenkette. */
  readonly erstelltAm: string;
  readonly schriftgroesse: Schriftgroesse;
}

/** Wie viele Läufe der Bericht auflistet. */
export const VERLAUF_HOECHSTENS = 20;

function ueberblick(u: Lernuebersicht): string {
  const offen = u.fragenGesamt - u.beantwortet;
  const zeilen: Zelle[][] = [
    [{ text: 'Fragen im Katalog' }, { text: String(u.fragenGesamt), zahl: true }],
    [{ text: 'Schon einmal beantwortet' }, { text: String(u.beantwortet), zahl: true }],
    [{ text: 'Noch nie beantwortet' }, { text: String(offen), zahl: true }],
    /* Ohne Farbe: Ein grünes „0“ läse sich als gute Nachricht, und die Zeile
       sagt schon selbst, worum es geht. Farbe trägt hier nichts bei – sie
       würde ein Urteil andeuten, das die Zahl nicht hergibt. */
    [{ text: 'Abruf belegt und frisch' }, { text: String(u.belegt), zahl: true }],
    [{ text: 'Heute zur Wiederholung fällig' }, { text: String(u.faellig), zahl: true }],
    [{ text: 'Auf der Merkliste' }, { text: String(u.gemerkt), zahl: true }],
  ];

  return abschnitt('Ihr Stand insgesamt', [
    absatz(`${STUFE_WORT[u.stufe]}: ${reifesatz(u.belegt, u.fragenGesamt, u.stufe, u.deckelnd)}`),
    /* Der Vorbehalt gehört in den Bericht, nicht nur auf den Bildschirm: Das
       PDF wird ausgedruckt, weitergereicht und später ohne die Anwendung
       daneben gelesen. Was es behauptet, muss für sich allein stimmen. */
    absatz(
      'Diese Zahl sagt, was belegt sitzt – nicht, was in der Prüfung herauskäme. Eine Frage ' +
        'zählt erst, wenn sie nach mindestens einem Tag Abstand richtig beantwortet wurde, und ' +
        'ihr Beitrag sinkt wieder, je länger das her ist. Geraten ist nicht eingerechnet, nie ' +
        'Gesehenes gilt als nicht gekonnt. Über das Bestehen entscheidet allein der ' +
        'Prüfungsausschuss.',
      'hinweis',
    ),
    tabelle('Übersicht über den Lernstand', ['Kennzahl', 'Anzahl'], zeilen),
  ]);
}

function bereiche(u: Lernuebersicht): string {
  if (u.bereiche.length === 0) {
    return abschnitt('Nach Bereichen', [
      absatz('Zu den einzelnen Bereichen liegen noch keine Zahlen vor.', 'hinweis'),
    ]);
  }

  const zeilen: Zelle[][] = u.bereiche.map((b) => [
    { text: `${b.id} – ${b.titel}` },
    { text: String(b.fragenGesamt), zahl: true },
    { text: String(b.beantwortet), zahl: true },
    { text: String(b.belegt), zahl: true },
    { text: STUFE_WORT[b.stufe] },
  ]);

  return abschnitt('Nach Bereichen', [
    absatz(
      'Die Bezeichnungen der Bereiche stammen aus dem amtlichen Fragenkatalog und sind unverändert übernommen.',
      'hinweis',
    ),
    tabelle(
      'Lernstand je Kapitel und Abschnitt',
      ['Bereich', 'Fragen', 'Beantwortet', 'Belegt', 'Stand'],
      zeilen,
    ),
  ]);
}

function planung(plan: Lernplan | null): string {
  if (plan === null) {
    return abschnitt('Ihr Lernplan', [
      absatz('Der Lernplan konnte für diesen Bericht nicht ermittelt werden.', 'hinweis'),
    ]);
  }

  const teile: string[] = [
    absatz(MACHBARKEIT_SATZ[plan.machbarkeit]),
    /* Ohne den Blick auf heute: Der steht als Zahl im Abschnitt davor, und
       seit dem Umbau ist es dieselbe Rechnung. Zweimal dieselbe Größe in
       zwei Einheiten – einmal als Fragenzahl, einmal als Prozentwert – war
       genau die Doppelung, die diesen Schritt ausgelöst hat. */
    absatz(`Angestrebt ist ein Abrufstand von ${prozent(plan.zielquote)} je Frage.`),
  ];

  if (plan.termin !== null) {
    const tage = plan.tageBisTermin;
    teile.push(
      absatz(
        `Prüfungstermin: ${deutschesDatum(plan.termin)}` +
          (tage === null ? '.' : ` – noch ${String(tage)} Tage.`),
      ),
    );
    if (plan.prognoseAmTermin !== null) {
      /* Die Prognose zum Termin gilt unter der Annahme, dass ab heute nicht
         mehr gelernt wird. Ohne diesen Zusatz läse sich die Zahl als
         Versprechen. */
      teile.push(
        absatz(
          `Ohne weiteres Lernen wären es am Prüfungstag noch ${prozent(plan.prognoseAmTermin)}.`,
        ),
      );
    }
  }

  teile.push(
    tabelle(
      'Empfohlenes Tagespensum',
      ['Anteil', 'Fragen'],
      [
        [{ text: 'Neue Fragen' }, { text: String(plan.pensum.neu), zahl: true }],
        [{ text: 'Wiederholungen' }, { text: String(plan.pensum.wiederholung), zahl: true }],
        [{ text: 'Zusammen' }, { text: String(plan.pensum.gesamt), zahl: true }],
        [
          { text: 'Geschätzte Dauer in Minuten' },
          { text: String(plan.pensum.minuten), zahl: true },
        ],
      ],
    ),
  );

  return abschnitt('Ihr Lernplan', teile);
}

function simulationen(verlauf: readonly Pruefungsverlauf[]): string {
  if (verlauf.length === 0) {
    return abschnitt('Prüfungssimulationen', [
      absatz('Es wurde noch keine Prüfungssimulation abgeschlossen.', 'hinweis'),
    ]);
  }

  const gezeigt = verlauf.slice(0, VERLAUF_HOECHSTENS);
  const zeilen: Zelle[][] = gezeigt.map((lauf) => [
    { text: deutschesDatum(lauf.zeitpunkt) },
    { text: lauf.profilName },
    /* Ohne die Dauer bliebe auf dem Papier unsichtbar, ob jemand von 118 auf
       87 Minuten heruntergekommen ist – Zeitnot ist ein eigenes
       Durchfallrisiko und nicht an der Trefferquote abzulesen. */
    { text: dauerInMinuten(lauf.dauerMs), zahl: true },
    { text: `${String(lauf.richtig)} von ${String(lauf.gesamt)}`, zahl: true },
    { text: prozent(lauf.quote), zahl: true },
    {
      /* Urteil als Wort, nicht als Farbe: Ein Graustufendruck macht aus
         Grün und Rot dasselbe Grau. */
      text: URTEIL_BEZEICHNUNG[lauf.urteil],
      klasse:
        lauf.urteil === 'bestanden' ? 'gut' : lauf.urteil === 'nicht_bestanden' ? 'schlecht' : '',
    },
  ]);

  const teile = [
    tabelle(
      'Abgeschlossene Prüfungssimulationen',
      ['Datum', 'Profil', 'Dauer', 'Richtig', 'Quote', 'Urteil'],
      zeilen,
    ),
  ];

  if (verlauf.length > gezeigt.length) {
    teile.push(
      absatz(
        `Angezeigt sind die ${String(gezeigt.length)} jüngsten von insgesamt ${String(verlauf.length)} Läufen.`,
        'hinweis',
      ),
    );
  }

  return abschnitt('Prüfungssimulationen', teile);
}

/**
 * Baut den Lernbericht als vollständige HTML-Datei.
 *
 * Enthält bewusst **keine** Frage im Wortlaut und keine Antwort: Der Bericht
 * soll den Stand zeigen, nicht den Katalog ersetzen. Dass er trotzdem die
 * Herkunft nennt, liegt an den Bereichsbezeichnungen, die aus dem amtlichen
 * Werk stammen.
 */
export function lernberichtBauen(daten: Lernberichtsdaten): string {
  return dokumentBauen({
    titel: 'Lernbericht Waffensachkunde',
    augenbraue: `${daten.profilName} · erstellt am ${deutschesDatum(daten.erstelltAm)}`,
    quelle: daten.quelle,
    schriftgroesse: daten.schriftgroesse,
    abschnitte: [
      ueberblick(daten.uebersicht),
      bereiche(daten.uebersicht),
      planung(daten.plan),
      simulationen(daten.verlauf),
    ],
  });
}

/** Vorschlag für den Dateinamen; enthält keine Zeichen, die Dateisysteme stören. */
export function dateiname(profilName: string, erstelltAm: string): string {
  const datum = Number.isNaN(Date.parse(erstelltAm))
    ? 'ohne-datum'
    : new Date(erstelltAm).toISOString().slice(0, 10);
  const name = profilName
    .normalize('NFKD')
    .replace(/[\u0300-\u036f]/gu, '')
    .replace(/[^A-Za-z0-9]+/gu, '-')
    .replace(/^-+|-+$/gu, '')
    .slice(0, 40);

  return `Lernbericht${name === '' ? '' : `-${name}`}-${datum}.pdf`;
}
