/**
 * Die mitgelieferten Normtexte – Vertrag und Auflösung einer Fundstelle.
 *
 * ## Wozu
 *
 * Jede Erklärungstafel schließt mit „Im Gesetz nachlesen" und einer Liste von
 * Fundstellen. Bis 0.22.0 waren das reine Textzitate ohne Sprungziel, und die
 * Anwendung lieferte keinen einzigen Normtext mit – der Lernende einer
 * ausdrücklich vollständig offline arbeitenden Software konnte die
 * Aufforderung also gerade nicht offline erfüllen.
 *
 * `content/normtexte.json` schließt die Lücke. Die Datei entsteht mit
 * `data-pipeline/normtexte_bauen.py` aus `content/gesetze/index.json` und
 * enthält **nur die zitierten Normen** – der Index selbst führt alle sieben
 * Gesetze vollständig, auch katalogfremde Normen wie § 173 StGB.
 *
 * ## Wortlaut
 *
 * Der Text ist der amtliche, unverändert. Gesetzestexte sind nach § 5 Abs. 1
 * UrhG gemeinfrei; verfälschen darf man sie deshalb trotzdem nicht.
 *
 * ## Warum Anlagen anders sind
 *
 * Anlagen kennen keine Absatzgliederung und sind seitenlang – Anlage 1 des
 * WaffG hat 29 000 Zeichen und wird 381-mal zitiert. Sie werden deshalb in
 * ihre eigenen Gliederungsblöcke zerlegt, und zwar **verlustfrei**: Die Blöcke
 * ergeben aneinandergehängt wieder Zeichen für Zeichen den Ausgangstext (die
 * Pipeline bricht ab, wenn das nicht stimmt). Ein Block ist ein Sprungziel,
 * kein Ausschnitt. Beschnitten wird nichts – ein falsch gesetzter Schnitt wäre
 * ein verfälschtes Gesetzeszitat, der schlimmste Fehler, den diese Anwendung
 * machen könnte.
 *
 * Der `pfad` ist dabei nicht schmückend: In Anlage 1 des WaffG kommt die Marke
 * „1.1" **vier**mal vor, in verschiedenen Unterabschnitten. Ohne ihn spränge
 * eine Fundstelle unbemerkt an die falsche Stelle des Gesetzes.
 */

import type { Fundstelle, Gesetzeskuerzel } from './erklaerungen';

/** Ein Gliederungsblock einer Anlage. */
export interface Anlagenblock {
  /** „1.3.1.3", „Abschnitt 2" – oder `null` für den Vorspann. */
  readonly marke: string | null;
  /** Die Abschnitte, in denen der Block steht, von außen nach innen. */
  readonly pfad: readonly string[];
  readonly text: string;
}

export interface Normtext {
  readonly titel: string;
  readonly istAnlage: boolean;
  /** Nur bei Paragrafen: Absatznummer -> Text. Leer bei Normen ohne Gliederung. */
  readonly absaetze?: Readonly<Record<string, string>>;
  /** Nur bei Paragrafen ohne Absatzgliederung: der ganze Text. */
  readonly text?: string;
  /** Nur bei Anlagen. */
  readonly bloecke?: readonly Anlagenblock[];
}

export interface Gesetzestext {
  readonly bezeichnung: string;
  /** Änderungsstand, wörtlich aus dem amtlichen XML. */
  readonly stand: string;
  readonly quelle: string;
  readonly normen: Readonly<Record<string, Normtext>>;
}

export interface NormtexteMeta {
  readonly version: number;
  readonly stand: string;
  readonly hinweis: string;
  readonly gesetzesstand: Readonly<Record<string, string>>;
}

export interface Normtexte {
  readonly meta: NormtexteMeta;
  readonly gesetze: Readonly<Record<string, Gesetzestext>>;
}

export const NORMTEXTE_LEER: Normtexte = Object.freeze({
  meta: Object.freeze({
    version: 0,
    stand: '',
    hinweis: '',
    gesetzesstand: Object.freeze({}),
  }),
  gesetze: Object.freeze({}),
});

/** Die Norm zu einer Fundstelle – oder `null`, wenn sie nicht mitgeliefert wird. */
export function normFinden(normtexte: Normtexte, fundstelle: Fundstelle): Normtext | null {
  return normtexte.gesetze[fundstelle.gesetz]?.normen[fundstelle.norm] ?? null;
}

/** Das Gesetz zu einer Fundstelle, für Bezeichnung, Stand und Quelle. */
export function gesetzFinden(normtexte: Normtexte, gesetz: Gesetzeskuerzel): Gesetzestext | null {
  return normtexte.gesetze[gesetz] ?? null;
}

const ABSCHNITT = /(?<!Unter)[Aa]bschnitt\s+([0-9IVX]+)/u;
const UNTERABSCHNITT = /Unterabschnitt\s+([0-9IVX]+)/u;
const NUMMER = /(?:Nr\.|Nummer)\s*(\d+(?:\.\d+)*)/gu;

/**
 * Sucht den Block, den eine Anlagen-Fundstelle meint.
 *
 * Die Regel ist dieselbe wie in `data-pipeline/normtexte_bauen.py`, nur
 * andersherum gelesen: Aus der Stellenangabe werden Abschnitt, Unterabschnitt
 * und die **tiefste** Nummer herausgelesen; gesucht wird der Block mit genau
 * dieser Marke, dessen Pfad die genannten Abschnitte enthält.
 *
 * Gibt es **keinen oder mehr als einen** Treffer, ist das Ergebnis `null` –
 * und die Anzeige zeigt die ganze Anlage und sagt, dass sie die Stelle nicht
 * genau treffen konnte. Lieber zu viel zeigen als an die falsche Stelle
 * springen: Von 632 Anlagenzitaten lösen sich 588 eindeutig auf, die
 * verbleibenden 44 sind auf eine bis auf einen Fall die Abbildungen der
 * Anlage II BeschussV (43 von 44), die im amtlichen XML nur als
 * Bildunterschrift stehen. Nachgemessen am 30.08.2026 mit `auszugBilden`
 * über alle 2137 Fundstellen aus Erklärungen und Glossar.
 */
export function blockFinden(bloecke: readonly Anlagenblock[], stelle: string): Anlagenblock | null {
  const abschnitt = ABSCHNITT.exec(stelle);
  const unterabschnitt = UNTERABSCHNITT.exec(stelle);

  const verlangt: string[] = [];
  if (abschnitt !== null) {
    verlangt.push(`Abschnitt ${abschnitt[1] ?? ''}`);
  }
  if (unterabschnitt !== null) {
    verlangt.push(`Unterabschnitt ${unterabschnitt[1] ?? ''}`);
  }

  /* Die tiefste Nummer gewinnt: „Abschnitt 1 Unterabschnitt 1 Nr. 1.3.1.3“
     meint den Block 1.3.1.3, nicht den Abschnitt. */
  const nummern = [...stelle.matchAll(NUMMER)].map((treffer) => treffer[1] ?? '');
  const letzte = nummern.at(-1);

  const ziel = letzte ?? verlangt.at(-1);
  if (ziel === undefined) {
    return null;
  }
  // Ohne Nummer ist der letzte genannte Abschnitt selbst das Ziel; er steht
  // über seinem Pfad, nicht darin.
  const aussen = letzte === undefined ? verlangt.slice(0, -1) : verlangt;

  const treffer = bloecke.filter(
    (block) => block.marke === ziel && aussen.every((teil) => block.pfad.includes(teil)),
  );

  return treffer.length === 1 ? (treffer[0] ?? null) : null;
}

/**
 * Zeigt die Stellenangabe auf eine Abbildung?
 *
 * Anlage II der BeschussV besteht aus den Beschuss- und Prüfzeichen. Das
 * amtliche XML führt sie nur als Bildunterschrift, ohne Bild – deshalb sind
 * fast alle nicht auflösbaren Anlagenzitate von dieser Art, und deshalb ist
 * es ehrlicher, den Grund zu nennen, als den Leser suchen zu lassen. Der
 * Befund steht in `docs/stand.md` 7.22.
 */
export function zeigtAufAbbildung(stelle: string): boolean {
  return /Abbildung/iu.test(stelle);
}

/** Was die Anzeige zu einer Fundstelle zeigen soll. */
export type Normauszug =
  /** Der zitierte Absatz eines Paragrafen. */
  | {
      readonly art: 'absatz';
      readonly titel: string;
      readonly absatz: string;
      readonly text: string;
    }
  /** Ein Paragraf, der als Ganzes zitiert wird oder keine Absätze hat. */
  | { readonly art: 'norm'; readonly titel: string; readonly text: string }
  /** Der getroffene Block einer Anlage. */
  | { readonly art: 'block'; readonly titel: string; readonly block: Anlagenblock }
  /** Die Anlage, deren Stelle sich nicht eindeutig auflösen ließ. */
  | {
      readonly art: 'anlage';
      readonly titel: string;
      readonly bloecke: readonly Anlagenblock[];
      /** Die Stellenangabe, die nicht traf – die Anzeige nennt sie. */
      readonly stelle: string;
      /**
       * Ob die Stelle auf eine Abbildung zeigt.
       *
       * Das ist der Regelfall der 44 nicht auflösbaren Zitate: Anlage II der
       * BeschussV besteht aus den Beschuss- und Prüfzeichen, und das amtliche
       * XML führt sie nur als Bildunterschrift ohne Bild. Die Anzeige sagt
       * das, statt den Leser suchen zu lassen.
       */
      readonly abbildung: boolean;
    }
  /** Zu dieser Fundstelle liegt kein Text vor. */
  | { readonly art: 'fehlt' };

/**
 * Löst eine Fundstelle in den anzuzeigenden Auszug auf.
 *
 * Reine Funktion: Sie entscheidet, **was** dasteht, nicht wie es aussieht.
 * Fehlt etwas, sagt sie das – erfunden wird nichts, und ein leerer Absatz
 * wird nicht als vorhandener ausgegeben.
 */
export function auszugBilden(normtexte: Normtexte, fundstelle: Fundstelle): Normauszug {
  const norm = normFinden(normtexte, fundstelle);
  if (norm === null) {
    return { art: 'fehlt' };
  }

  if (norm.istAnlage) {
    const bloecke = norm.bloecke ?? [];
    if (bloecke.length === 0) {
      return { art: 'fehlt' };
    }
    const stelle = fundstelle.stelle ?? '';
    const block = stelle.length > 0 ? blockFinden(bloecke, stelle) : null;
    return block === null
      ? {
          art: 'anlage',
          titel: norm.titel,
          bloecke,
          stelle,
          abbildung: zeigtAufAbbildung(stelle),
        }
      : { art: 'block', titel: norm.titel, block };
  }

  const absaetze = norm.absaetze ?? {};
  const gesucht = fundstelle.absatz;
  if (gesucht !== undefined) {
    const text = absaetze[gesucht];
    return text === undefined
      ? { art: 'fehlt' }
      : { art: 'absatz', titel: norm.titel, absatz: gesucht, text };
  }

  /* Ohne Absatzangabe: Normen ohne Gliederung tragen ihren Text direkt,
     gegliederte werden der Reihe nach zusammengesetzt. Die Absatznummern
     stehen im amtlichen Text ohnehin in Klammern voran. */
  if (norm.text !== undefined && norm.text.length > 0) {
    return { art: 'norm', titel: norm.titel, text: norm.text };
  }
  const zusammen = Object.entries(absaetze)
    .map(([nummer, text]) => `(${nummer}) ${text}`)
    .join('\n\n');
  return zusammen.length === 0
    ? { art: 'fehlt' }
    : { art: 'norm', titel: norm.titel, text: zusammen };
}
