"""Baut ``content/normtexte.json`` – die Normtexte, die mitgeliefert werden.

Warum es diese Datei gibt
-------------------------
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.

Die Datenbasis liegt seit jeher vor: ``content/gesetze/index.json`` ist die
Prüfgrundlage jeder einzelnen Fundstelle. Sie enthält die sieben Gesetze aber
**vollständig** – 914 Normen, darunter katalogfremde wie § 173 StGB. Diese
Datei hier ist der Auszug: nur die Normen, die in ``erklaerungen.json`` oder
``glossar.json`` wirklich zitiert werden. Zitiert wird nichts hinzugefügt und
nichts umformuliert; Gesetzestexte sind nach § 5 Abs. 1 UrhG gemeinfrei.

Die Anlagen sind der schwierige Teil
------------------------------------
632 der 2137 Fundstellen zeigen in eine Anlage, allein 391 in Anlage 1 des
WaffG – und die ist 29 000 Zeichen lang. Sie als eine Textwand anzuzeigen
hilft niemandem, am wenigsten mit Bildschirmleser.

Die Versuchung wäre, den zitierten Ausschnitt herauszuschneiden. Genau das
macht dieses Skript **nicht**. Ein falsch gesetzter Schnitt wäre ein
verfälschtes Gesetzeszitat in einer Lernsoftware für eine Rechtsprüfung – der
schlimmste Fehler, den diese Anwendung machen könnte. Stattdessen wird die
Anlage **verlustfrei** in ihre eigenen Gliederungsblöcke zerlegt: Aneinander
gehängt ergeben die Blöcke wieder Zeichen für Zeichen den Ausgangstext, und
genau das prüft das Skript, bevor es etwas schreibt. Die Blockmarken sind für
die Anwendung nur Sprungziele. Beschnitten wird nichts.

Aufruf
------
    python data-pipeline/normtexte_bauen.py
    python data-pipeline/normtexte_bauen.py --selbsttest
"""

from __future__ import annotations

import json
import re
import sys
from datetime import date, timezone, datetime
from pathlib import Path

WURZEL = Path(__file__).resolve().parent.parent
INDEX = WURZEL / 'content' / 'gesetze' / 'index.json'
ERKLAERUNGEN = WURZEL / 'content' / 'erklaerungen.json'
GLOSSAR = WURZEL / 'content' / 'glossar.json'
ZIEL = WURZEL / 'content' / 'normtexte.json'

#: Eine Gliederungsmarke am Zeilenanfang: „1.", „1.1", „2.3.4".
#: Die Anlagen setzen sie auf eine eigene Zeile, der Text folgt darunter.
MARKE = re.compile(r'^(\d+(?:\.\d+)*)\.?$')

#: „Abschnitt 1:", „Unterabschnitt 3:" – ebenfalls auf eigener Zeile.
GLIEDERUNGSZEILE = re.compile(r'^((?:Unter)?[Aa]bschnitt)\s+([0-9IVX]+)\s*:?$')

HINWEIS = (
    'Maschinell erzeugter Auszug aus dem amtlichen XML von gesetze-im-internet.de '
    '(Bundesministerium der Justiz / juris GmbH). Enthaelt ausschliesslich die Normen, '
    'die in den Erklaerungen oder im Glossar dieser Software zitiert werden. Der '
    'Wortlaut ist unveraendert; Gesetzestexte sind nach § 5 Abs. 1 UrhG gemeinfrei. '
    'Nicht von Hand aendern - neu erzeugen mit: python data-pipeline/normtexte_bauen.py'
)


def zitierte_normen() -> dict[str, set[str]]:
    """Alle Normen, die irgendwo zitiert werden – Gesetz -> Menge der Normnamen."""
    zitiert: dict[str, set[str]] = {}

    erklaerungen = json.loads(ERKLAERUNGEN.read_text(encoding='utf-8'))
    for eintrag in erklaerungen['zuFrage'].values():
        for fundstelle in eintrag.get('fundstellen', []):
            zitiert.setdefault(fundstelle['gesetz'], set()).add(fundstelle['norm'])

    glossar = json.loads(GLOSSAR.read_text(encoding='utf-8'))
    for eintrag in glossar['eintraege']:
        for fundstelle in eintrag.get('fundstellen', []):
            zitiert.setdefault(fundstelle['gesetz'], set()).add(fundstelle['norm'])

    return zitiert


def anlage_zerlegen(text: str) -> list[dict]:
    """Zerlegt eine Anlage verlustfrei in ihre Gliederungsblöcke.

    Jeder Block trägt die Marke, unter der er in der Anlage steht („1.3.1.3",
    „Abschnitt 2"), und seinen Text. **Verlustfrei** heißt: Die Texte aller
    Blöcke, in dieser Reihenfolge mit Zeilenumbruch verbunden, ergeben wieder
    genau den Ausgangstext. Der Aufrufer prüft das, bevor etwas geschrieben
    wird – ein beschnittenes Gesetzeszitat wäre der schlimmste Fehler, den
    diese Anwendung machen kann.

    Der Vorspann vor der ersten Marke bekommt ``marke = None``. Er fällt
    dadurch nicht weg, er ist nur kein Sprungziel.

    Jeder Block trägt zusätzlich seinen ``pfad`` – die Abschnitte, in denen er
    steht. Das ist keine Zugabe, sondern notwendig: In Anlage 1 des WaffG
    kommt die Marke „1.1" **vier**mal vor, in verschiedenen Unterabschnitten.
    Ohne Pfad spränge eine Fundstelle in die falsche Stelle des Gesetzes, und
    zwar unbemerkt.
    """
    bloecke: list[dict] = []
    pfad: list[str] = []
    laufend: dict = {'marke': None, 'pfad': [], 'zeilen': []}

    for zeile in text.split('\n'):
        roh = zeile.strip()
        marke: str | None = None

        treffer = MARKE.match(roh)
        if treffer is not None:
            marke = treffer.group(1)
        else:
            gliederung = GLIEDERUNGSZEILE.match(roh)
            if gliederung is not None:
                wort = gliederung.group(1).capitalize()
                marke = f'{wort} {gliederung.group(2)}'
                # „Abschnitt 2" beginnt von vorn, „Unterabschnitt 3" haengt
                # sich an den laufenden Abschnitt.
                pfad = [marke] if wort == 'Abschnitt' else [*pfad[:1], marke]

        if marke is not None:
            # Der bisherige Block ist zu Ende – auch ein leerer Vorspann wird
            # aufgehoben, sonst ginge seine Zeile verloren.
            if laufend['zeilen'] or laufend['marke'] is not None:
                bloecke.append(laufend)
            # Der Abschnittsblock selbst steht ueber seinem Pfad, nicht darin.
            eigener = pfad[:-1] if marke == (pfad[-1] if pfad else None) else list(pfad)
            laufend = {'marke': marke, 'pfad': eigener, 'zeilen': [zeile]}
        else:
            laufend['zeilen'].append(zeile)

    if laufend['zeilen'] or laufend['marke'] is not None:
        bloecke.append(laufend)

    return [
        {'marke': block['marke'], 'pfad': block['pfad'], 'text': '\n'.join(block['zeilen'])}
        for block in bloecke
    ]


def zusammensetzen(bloecke: list[dict]) -> str:
    """Die Gegenprobe zu :func:`anlage_zerlegen`."""
    return '\n'.join(block['text'] for block in bloecke)


def bauen() -> dict:
    index = json.loads(INDEX.read_text(encoding='utf-8'))
    zitiert = zitierte_normen()

    gesetze: dict[str, dict] = {}
    fehlend: list[str] = []
    anlagen = 0
    paragrafen = 0

    for kuerzel in sorted(zitiert):
        quelle = index['gesetze'].get(kuerzel)
        if quelle is None:
            fehlend.append(kuerzel)
            continue

        normen: dict[str, dict] = {}
        for name in sorted(zitiert[kuerzel], key=sortierschluessel):
            norm = quelle['normen'].get(name)
            if norm is None:
                fehlend.append(f'{kuerzel} {name}')
                continue

            if norm['ist_anlage']:
                bloecke = anlage_zerlegen(norm['text'])
                # Die Gegenprobe. Sie steht hier und nicht im Selbsttest:
                # Sie muss fuer jede ausgelieferte Anlage gelten, nicht nur
                # fuer ein Beispiel.
                if zusammensetzen(bloecke) != norm['text']:
                    raise SystemExit(
                        f'ABBRUCH: Die Zerlegung von {kuerzel} {name} ist nicht verlustfrei. '
                        'Es wird nichts geschrieben.'
                    )
                normen[name] = {
                    'titel': norm['titel'],
                    'istAnlage': True,
                    'bloecke': bloecke,
                }
                anlagen += 1
            else:
                # Alle Absaetze der zitierten Norm, nicht nur die zitierten:
                # Wer einen Absatz nachschlaegt, liest oft den daneben mit -
                # und eine halb ausgelieferte Norm waere eine zweite Sorte
                # Luecke, die niemand erwartet.
                normen[name] = {
                    'titel': norm['titel'],
                    'istAnlage': False,
                    'absaetze': {
                        nummer: absatz['text'] for nummer, absatz in norm['absaetze'].items()
                    },
                }
                # Normen ganz ohne Absatzgliederung tragen ihren Text im Feld
                # „text"; ohne diesen Zweig kaemen sie leer heraus.
                if not normen[name]['absaetze']:
                    normen[name]['text'] = norm['text']
                paragrafen += 1

        gesetze[kuerzel] = {
            'bezeichnung': quelle['bezeichnung'],
            'stand': quelle['stand'],
            'quelle': quelle['quelle'],
            'normen': normen,
        }

    if fehlend:
        raise SystemExit(f'ABBRUCH: nicht im Index gefunden: {", ".join(fehlend)}')

    print(f'{paragrafen} Paragrafen und {anlagen} Anlagen aus {len(gesetze)} Gesetzen.')

    return {
        'meta': {
            'version': 1,
            'stand': date.today().isoformat(),
            'hinweis': HINWEIS,
            'gesetzesstand': {k: g['stand'] for k, g in gesetze.items()},
        },
        'gesetze': gesetze,
    }


def sortierschluessel(name: str) -> tuple:
    """Normen in ihrer natürlichen Reihenfolge: § 2 vor § 10, Anlagen zuletzt."""
    treffer = re.match(r'^§ (\d+)([a-z]*)$', name)
    if treffer is not None:
        return (0, int(treffer.group(1)), treffer.group(2))
    return (1, 0, name)


def selbsttest() -> int:
    """Prüft die Zerlegung an Fällen, deren Ergebnis von Hand feststeht."""
    fehler = 0

    def pruefe(name: str, bedingung: bool) -> None:
        nonlocal fehler
        if not bedingung:
            print(f'  FEHLER: {name}')
            fehler += 1
        else:
            print(f'  ok: {name}')

    text = 'Vorspann\nAbschnitt 1:\nÜberschrift\n1.1\nErster Satz.\n1.2\nZweiter Satz.'
    bloecke = anlage_zerlegen(text)
    pruefe('verlustfrei', zusammensetzen(bloecke) == text)
    pruefe('Vorspann bleibt erhalten', bloecke[0]['marke'] is None)
    pruefe('Abschnitt wird erkannt', bloecke[1]['marke'] == 'Abschnitt 1')
    pruefe('Nummern werden erkannt', [b['marke'] for b in bloecke[2:]] == ['1.1', '1.2'])
    pruefe(
        'die Marke bleibt im Text stehen',
        bloecke[2]['text'].startswith('1.1'),
    )

    tief = '1.3\nEbene zwei\n1.3.1.3\nEbene vier'
    pruefe('tiefe Nummern', [b['marke'] for b in anlage_zerlegen(tief)] == ['1.3', '1.3.1.3'])

    # Eine Zeile, die nur zufaellig mit einer Zahl beginnt, ist keine Marke.
    kein = '1. Januar 2026 gilt Folgendes\nWeiter im Text'
    pruefe('kein Fehlalarm bei Fliesstext', anlage_zerlegen(kein)[0]['marke'] is None)

    leer = ''
    pruefe('leerer Text bleibt verlustfrei', zusammensetzen(anlage_zerlegen(leer)) == leer)

    return fehler


def main() -> int:
    if '--selbsttest' in sys.argv:
        fehler = selbsttest()
        print('Selbsttest bestanden.' if fehler == 0 else f'{fehler} Fehler.')
        return 1 if fehler else 0

    daten = bauen()
    ZIEL.write_text(
        json.dumps(daten, ensure_ascii=False, indent=1) + '\n',
        encoding='utf-8',
        # LF wie im Archiv verlangt, nicht das CRLF von Windows.
        newline='\n',
    )
    groesse = ZIEL.stat().st_size
    print(f'Geschrieben: {ZIEL.relative_to(WURZEL)} ({groesse / 1024:.0f} KiB)')
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
