/**
 * Prüfhilfen für Nutzlasten an der IPC-Grenze.
 *
 * Jede Nutzlast aus dem Renderer ist unbekannt, bis sie geprüft wurde. Die
 * hier gesammelten Funktionen nehmen deshalb `unknown` entgegen, liefern
 * einen engen Typ zurück und brechen sonst mit einer deutschen Meldung ab.
 *
 * Bewusst ein eigenes Modul: Lernstand und Prüfungssimulation prüfen ihre
 * Eingaben nach denselben Regeln, und zwei Kopien derselben Regel driften
 * mit der Zeit auseinander.
 */

/** Bricht die Verarbeitung mit einer sprechenden deutschen Meldung ab. */
export function abweisen(nachricht: string): never {
  throw new Error(nachricht);
}

/** Stellt sicher, dass die Nutzlast ein einfaches Objekt ist. */
export function nutzlast(wert: unknown, name: string): Record<string, unknown> {
  if (typeof wert !== 'object' || wert === null || Array.isArray(wert)) {
    abweisen(`Ungültige Anfrage: ${name} muss ein Objekt sein.`);
  }
  return wert as Record<string, unknown>;
}

export function ganzeZahl(wert: unknown, name: string, min: number, max: number): number {
  if (typeof wert !== 'number' || !Number.isInteger(wert) || wert < min || wert > max) {
    abweisen(
      `Ungültige Anfrage: ${name} muss eine ganze Zahl zwischen ${String(min)} und ${String(max)} sein.`,
    );
  }
  return wert;
}

/** Wie {@link ganzeZahl}, lässt aber Nachkommastellen zu (etwa für Quoten). */
export function endlicheZahl(wert: unknown, name: string, min: number, max: number): number {
  if (typeof wert !== 'number' || !Number.isFinite(wert) || wert < min || wert > max) {
    abweisen(
      `Ungültige Anfrage: ${name} muss eine Zahl zwischen ${String(min)} und ${String(max)} sein.`,
    );
  }
  return wert;
}

export function wahrheitswert(wert: unknown, name: string, standard: boolean): boolean {
  if (wert === undefined || wert === null) {
    return standard;
  }
  if (typeof wert !== 'boolean') {
    abweisen(`Ungültige Anfrage: ${name} muss ein Wahrheitswert sein.`);
  }
  return wert;
}

export function textliste(wert: unknown, name: string): string[] {
  if (wert === undefined || wert === null) {
    return [];
  }
  if (!Array.isArray(wert)) {
    abweisen(`Ungültige Anfrage: ${name} muss eine Liste von Zeichenketten sein.`);
  }
  return (wert as readonly unknown[]).map((eintrag) => {
    if (typeof eintrag !== 'string') {
      abweisen(`Ungültige Anfrage: ${name} enthält einen Eintrag, der keine Zeichenkette ist.`);
    }
    return eintrag;
  });
}

/** Obergrenze einer bereinigten Meldung, in Graphemen. */
const MAX_MELDUNGSLAENGE = 80;

/**
 * Zeichen, die in einer Meldung nichts zu suchen haben.
 *
 * Neben den klassischen Steuerzeichen (C0 und DEL) auch:
 *
 * - **C1** (U+0080–U+009F) – in manchen Terminals als Steuerbefehl gedeutet.
 * - **U+2028/U+2029** – Zeilen- und Absatztrenner; sie brechen eine Logzeile
 *   auf, ohne wie ein Zeilenumbruch auszusehen.
 * - **Bidirektionale Steuerzeichen** (U+200E/U+200F, U+202A–U+202E,
 *   U+2066–U+2069) – mit ihnen lässt sich die Anzeigereihenfolge umkehren.
 *   Eine Meldung kann dann etwas völlig anderes zeigen, als sie enthält.
 * - **U+FEFF** – unsichtbar und in Textvergleichen leicht zu übersehen.
 */
function istGefaehrlich(punkt: number): boolean {
  return (
    punkt < 0x20 ||
    punkt === 0x7f ||
    (punkt >= 0x80 && punkt <= 0x9f) ||
    punkt === 0x200e ||
    punkt === 0x200f ||
    (punkt >= 0x202a && punkt <= 0x202e) ||
    punkt === 0x2028 ||
    punkt === 0x2029 ||
    (punkt >= 0x2066 && punkt <= 0x2069) ||
    punkt === 0xfeff
  );
}

/**
 * Ersetzt Steuerzeichen und kürzt auf 80 Zeichen.
 *
 * Fremdeingaben landen in Fehlermeldungen, im Protokoll und in Profilnamen.
 * Ohne diese Reinigung könnte eine geschickt gewählte Zeichenkette Log-Zeilen
 * oder Terminalausgaben verfälschen – im Fall der bidirektionalen
 * Steuerzeichen sogar so, dass die Anzeige das Gegenteil des Inhalts zeigt.
 *
 * Gekürzt wird nach **Graphemen**, nicht nach UTF-16-Einheiten und auch nicht
 * nach Codepoints. Nach UTF-16-Einheiten bliebe womöglich ein halbes
 * Surrogatpaar zurück; nach Codepoints zerfiele eine zusammengesetzte
 * Darstellung – eine Flagge, eine Familie, ein Buchstabe mit Akzent – in ihre
 * Bestandteile und zeigte etwas anderes an als vorher.
 */
const SEGMENTIERER = new Intl.Segmenter('de', { granularity: 'grapheme' });

export function entschaerft(wert: unknown, ersatz = '?'): string {
  const text = typeof wert === 'string' ? wert : String(wert);

  let sauber = '';
  let gezaehlt = 0;

  for (const { segment } of SEGMENTIERER.segment(text)) {
    if (gezaehlt >= MAX_MELDUNGSLAENGE) {
      break;
    }
    gezaehlt += 1;

    /* Ein Graphem kann aus mehreren Codepoints bestehen. Steckt auch nur ein
       gefährlicher darin, wird das ganze Graphem ersetzt – ein bidirektionales
       Steuerzeichen wirkt sonst weiter, nur eben eingebettet. */
    let gefaehrlich = false;
    for (const zeichen of segment) {
      if (istGefaehrlich(zeichen.codePointAt(0) ?? 0)) {
        gefaehrlich = true;
        break;
      }
    }
    sauber += gefaehrlich ? ersatz : segment;
  }

  return sauber;
}

/** Fisher-Yates. Liefert eine neue Liste, die Vorlage bleibt unberührt. */
export function gemischt<T>(werte: readonly T[], zufall: () => number): T[] {
  const kopie = [...werte];
  for (let i = kopie.length - 1; i > 0; i -= 1) {
    const j = Math.floor(zufall() * (i + 1));
    const a = kopie[i];
    const b = kopie[j];
    if (a !== undefined && b !== undefined) {
      kopie[i] = b;
      kopie[j] = a;
    }
  }
  return kopie;
}
