/**
 * Ersatzweises Zusammenstellen eines Prüfungsbogens – reine Funktionen.
 *
 * Regelfall ist der Kanal `pruefung:starten`: der Anwendungskern kennt den
 * vollständigen Katalog und den Lernstand. Steht er nicht bereit, stellt die
 * Oberfläche den Bogen selbst zusammen, damit sich trotzdem üben lässt. Die
 * Auswahl ist dann eine Näherung – darauf weist die Prüfungsansicht hin.
 */

import type { Fragetyp, Frage, Katalog } from '../../../shared/katalog';
import {
  gewuenschteTypen,
  type Pruefungsauftrag,
  type Pruefungsfrage,
  type Pruefungsprofil,
} from '../../../shared/pruefung';
import { mischen } from '../lernen/sitzungsaufbau';

/** Gehört die Frage zu diesem Kapitel oder Abschnitt? */
export function gehoertZuBereich(frage: Frage, bereich: string): boolean {
  return frage.kapitel === bereich || frage.abschnitt === bereich;
}

function zuBogenfrage(frage: Frage, optionenMischen: boolean): Pruefungsfrage {
  const labels = (frage.optionen ?? []).map((option) => option.label);
  return {
    frageId: frage.id,
    optionsReihenfolge: optionenMischen ? mischen(labels) : labels,
  };
}

/** Füllt bis zur Zielzahl mit noch nicht verwendeten Fragen auf. */
function auffuellen(
  gewaehlt: Frage[],
  benutzt: Set<string>,
  vorrat: readonly Frage[],
  anzahl: number,
): void {
  for (const frage of mischen(vorrat)) {
    if (gewaehlt.length >= anzahl) {
      return;
    }
    if (!benutzt.has(frage.id)) {
      gewaehlt.push(frage);
      benutzt.add(frage.id);
    }
  }
}

/**
 * Verteilt das bogenweite Ziel offener Fragen auf die Bereiche.
 *
 * Anteilig zur Bereichsgröße; was ein Bereich mangels offener Fragen nicht
 * tragen kann, übernehmen die übrigen. Ohne diese Umverteilung verfiele der
 * Fehlbetrag in kleinen Bereichen – genau der Befund, der den Anwendungskern
 * schon einmal auf 18 statt 20 offene Fragen brachte (siehe
 * `Pruefung.offeneJeBereich` und tests/bogen-echt.test.ts).
 */
function offeneJeBereich(
  teile: readonly { readonly anzahl: number; readonly offenVerfuegbar: number }[],
  offenZiel: number,
): number[] {
  const gesamt = teile.reduce((summe, teil) => summe + teil.anzahl, 0);
  const tragbar = (teil: { readonly anzahl: number; readonly offenVerfuegbar: number }): number =>
    Math.min(teil.anzahl, teil.offenVerfuegbar);

  const ergebnis = teile.map((teil) =>
    Math.min(tragbar(teil), gesamt > 0 ? Math.round((teil.anzahl / gesamt) * offenZiel) : 0),
  );

  // Rundungsrest und nicht tragbare Anteile an Bereiche mit Reserve weiterreichen.
  let rest = offenZiel - ergebnis.reduce((summe, zahl) => summe + zahl, 0);
  for (let i = 0; i < teile.length && rest > 0; i += 1) {
    const teil = teile[i];
    const bisher = ergebnis[i];
    if (teil === undefined || bisher === undefined) {
      continue;
    }
    const zuschlag = Math.min(tragbar(teil) - bisher, rest);
    ergebnis[i] = bisher + zuschlag;
    rest -= zuschlag;
  }

  return ergebnis;
}

/**
 * Auswahl nach festen Themenquoten, wie sie manche Träger vorgeben.
 *
 * Die Quoten sind die härtere Vorgabe, der zugesagte Anteil offener Fragen
 * gilt aber trotzdem: Beide Vorgaben zusammen sind das, was der
 * Anwendungskern liefert, und der Ersatzweg darf davon nicht abweichen.
 */
function nachThemenquoten(
  vorrat: readonly Frage[],
  quoten: Readonly<Record<string, number>>,
  anzahl: number,
  anteilOffen: number,
): Frage[] {
  const gewaehlt: Frage[] = [];
  const benutzt = new Set<string>();

  const teile = Object.entries(quoten).map(([bereich, menge]) => {
    const teilVorrat = mischen(vorrat.filter((frage) => gehoertZuBereich(frage, bereich)));
    return {
      teilVorrat,
      anzahl: Math.min(menge, teilVorrat.length),
      offenVerfuegbar: teilVorrat.filter((frage) => frage.typ !== 'mc').length,
    };
  });

  const offenJeTeil = offeneJeBereich(
    teile,
    Math.round(teile.reduce((summe, teil) => summe + teil.anzahl, 0) * anteilOffen),
  );

  teile.forEach((teil, i) => {
    const frei = teil.teilVorrat.filter((frage) => !benutzt.has(frage.id));
    const offene = frei.filter((frage) => frage.typ !== 'mc');
    const ankreuzbare = frei.filter((frage) => frage.typ === 'mc');
    const offenSoll = offenJeTeil[i] ?? 0;

    /* Erst die zugeteilten offenen Fragen, dann Multiple Choice, dann die
       übrigen offenen: So trifft der Bereich seine Quote und der Bogen
       zugleich den zugesagten Anteil. */
    const auswahl = [...offene.slice(0, offenSoll), ...ankreuzbare, ...offene.slice(offenSoll)];
    for (const frage of auswahl.slice(0, teil.anzahl)) {
      gewaehlt.push(frage);
      benutzt.add(frage.id);
    }
  });

  auffuellen(gewaehlt, benutzt, vorrat, anzahl);
  return gewaehlt.slice(0, anzahl);
}

/** Auswahl mit vorgegebenem Anteil offener Fragen. */
function nachTypanteil(vorrat: readonly Frage[], anteilOffen: number, anzahl: number): Frage[] {
  if (anteilOffen <= 0) {
    return mischen(vorrat).slice(0, anzahl);
  }

  const offenZiel = Math.min(anzahl, Math.round(anzahl * anteilOffen));
  const offene = mischen(vorrat.filter((frage) => frage.typ !== 'mc')).slice(0, offenZiel);
  const ankreuzbare = mischen(vorrat.filter((frage) => frage.typ === 'mc')).slice(
    0,
    anzahl - offene.length,
  );

  const gewaehlt = [...offene, ...ankreuzbare];
  const benutzt = new Set(gewaehlt.map((frage) => frage.id));
  auffuellen(gewaehlt, benutzt, vorrat, anzahl);
  return gewaehlt.slice(0, anzahl);
}

/**
 * Stellt einen Bogen aus dem Katalog zusammen.
 *
 * `profil` muss bereits das wirksame Profil sein (siehe `wirksamesProfil`),
 * damit frei eingestellte Fragenzahlen tatsächlich greifen.
 */
export function bogenAusKatalog(
  katalog: Katalog,
  profil: Pruefungsprofil,
  auftrag: Pruefungsauftrag,
): Pruefungsfrage[] {
  const ausgeschlossen = new Set(auftrag.kapitelAusschluss ?? []);
  const typen = new Set<Fragetyp>(gewuenschteTypen(profil));
  const vorrat = katalog.fragen.filter(
    (frage) => !ausgeschlossen.has(frage.kapitel) && typen.has(frage.typ),
  );

  const anzahl = Math.max(1, profil.fragenAnzahl);
  // Ohne offene Fragetypen besteht der Bogen ausschließlich aus Multiple Choice.
  const anteilOffen = typen.has('freitext') ? (profil.anteilOffen ?? 0) : 0;
  const gewaehlt = profil.themenquoten
    ? nachThemenquoten(vorrat, profil.themenquoten, anzahl, anteilOffen)
    : nachTypanteil(vorrat, anteilOffen, anzahl);

  return mischen(gewaehlt).map((frage) => zuBogenfrage(frage, auftrag.optionenMischen ?? false));
}
