"""Erzeugt aus `docs/datenschutz.md` die veröffentlichungsfertige HTML-Fassung.

## Warum es dieses Werkzeug gibt

Der Microsoft Store verlangt die Datenschutzerklärung unter einer öffentlich
erreichbaren Adresse. Die Quelle ist Markdown und bleibt es — dort steht sie
neben dem Quelltext, auf den sie sich beruft, und wird mit ihm zusammen
geändert. Was ins Netz geht, ist HTML.

Von Hand übertragen wird das genau einmal gut und danach nie wieder: Bei der
zweiten Änderung weicht die Webseite vom Dokument ab, und dann behauptet eine
Datenschutzerklärung etwas anderes als die Datei, die sie belegt. Deshalb ein
Umsetzer statt einer Handarbeit — er ist wiederholbar, und die Webfassung ist
jederzeit neu erzeugbar statt nachgepflegt.

## Warum ein eigener statt einer Bibliothek

`python-markdown` und `marked` sind hier nicht installiert, und für ein
einzelnes Dokument eine Abhängigkeit aufzunehmen, die dann im Gate mitgeprüft
und in der Lizenzübersicht geführt werden müsste, steht in keinem Verhältnis.
Umgesetzt wird deshalb genau der Vorrat an Auszeichnungen, den dieses eine
Dokument benutzt — am Bestand nachgezählt, nicht geraten: eine H1, dreizehn
H2, neunzehn H3, einunddreißig Listenpunkte, zwei Tabellen, drei
Codeblöcke, dreizehn Trennlinien, achtundfünfzig Code-Auszeichnungen im
Fließtext, siebzig Fettungen, ein Link und eine Adressangabe in spitzen
Klammern. Kursivschrift, verschachtelte Listen und Blockzitate kommen nicht
vor; taucht eines davon auf, meldet der Umsetzer es, statt es stillschweigend
fallen zu lassen.

## Was hier nicht mitgeht

Alles oberhalb der doppelten Trennlinie in `docs/datenschutz.md`. Das ist der
Vorspann für den Betreiber — Arbeitsanweisung, keine Erklärung. Er hat auf
einer Webseite nichts verloren, und der Umsetzer bricht ab, wenn er die
Trennlinie nicht findet, statt versehentlich alles mitzunehmen.

Aufruf: `python tools/datenschutz_html.py`
"""

from __future__ import annotations

import re
import sys
from pathlib import Path

WURZEL = Path(__file__).resolve().parent.parent
QUELLE = WURZEL / "docs" / "datenschutz.md"
ZIEL = WURZEL / "docs" / "datenschutz.html"

# Die doppelte Trennlinie trennt den Betreibervorspann vom Erklärungstext.
TRENNER = "---\n---\n"

TITEL = "Datenschutzerklärung zur Waffensachkunde-Lernsoftware"


def veroeffentlichungsteil(markdown: str) -> str:
    """Alles unterhalb der doppelten Trennlinie – oder ein Abbruch."""
    stelle = markdown.find(TRENNER)
    if stelle < 0:
        sys.exit(
            "Die doppelte Trennlinie '---/---' fehlt in docs/datenschutz.md. "
            "Ohne sie ist nicht feststellbar, wo der Betreibervorspann endet; "
            "der Umsetzer bricht lieber ab, als ihn mitzuveröffentlichen."
        )
    return markdown[stelle + len(TRENNER) :].strip("\n")


def maskieren(text: str) -> str:
    """Zeichen, die in HTML eine Bedeutung haben, unschädlich machen.

    Zuerst und ausnahmslos: Das Dokument enthält an mehreren Stellen spitze
    Klammern als Text (`<Ihr Name>`, `<Zeitstempel>`, `<code>`). Würden sie
    als Auszeichnung durchgereicht, verschwänden sie beim Anzeigen spurlos –
    aus einem Dateinamen mit Platzhalter würde ein halber Dateiname.
    """
    return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")


def fliesstext(text: str) -> str:
    """Auszeichnungen innerhalb einer Zeile.

    Reihenfolge ist wesentlich: Code-Auszeichnungen zuerst und danach
    unangetastet, sonst würde ein `**` innerhalb eines Codeschnipsels als
    Fettung gelesen. Die Schnipsel werden deshalb herausgenommen, der Rest
    wird bearbeitet, und am Ende kommen sie zurück.
    """
    schnipsel: list[str] = []

    def beiseite(treffer: re.Match[str]) -> str:
        schnipsel.append(treffer.group(1))
        return f"\x00{len(schnipsel) - 1}\x00"

    text = re.sub(r"`([^`]+)`", beiseite, text)
    text = maskieren(text)

    # Adressangabe in spitzen Klammern – nach dem Maskieren also &lt;…&gt;.
    text = re.sub(
        r"&lt;(https?://[^\s&]+(?:&amp;[^\s&]+)*)&gt;",
        lambda t: f'<a href="{t.group(1)}">{t.group(1)}</a>',
        text,
    )
    # [Beschriftung](Adresse)
    text = re.sub(
        r"\[([^\]]+)\]\((https?://[^)]+)\)",
        lambda t: f'<a href="{t.group(2)}">{t.group(1)}</a>',
        text,
    )
    text = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", text)

    for nummer, inhalt in enumerate(schnipsel):
        text = text.replace(f"\x00{nummer}\x00", f"<code>{maskieren(inhalt)}</code>")
    return text


def kennung(ueberschrift: str) -> str:
    """Sprungziel aus einer Überschrift – damit Abschnitte verlinkbar sind."""
    roh = re.sub(r"<[^>]+>", "", ueberschrift).lower()
    ersetzungen = {"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss"}
    for zeichen, ersatz in ersetzungen.items():
        roh = roh.replace(zeichen, ersatz)
    roh = re.sub(r"[^a-z0-9]+", "-", roh).strip("-")
    return roh or "abschnitt"


def tabelle(zeilen: list[str]) -> list[str]:
    """Eine Pipe-Tabelle – ausgegeben als Liste, nicht als `<table>`.

    ## Warum keine Tabelle

    Das Redaktionssystem, in dem diese Erklärung erscheint, entfernt
    `<table>` beim Speichern und lässt nur den Zellinhalt stehen. Am
    28.08.2026 an der veröffentlichten Seite gemessen: null `<table>`-
    Elemente, aber der gesamte Zelltext vorhanden – aneinandergeklebt. Aus
    der Standtabelle wurde „Fassung der Erklärung1.1Stand28.08.2026“. `<ul>`,
    `<li>`, `<pre>`, `<code>` und `<strong>` überstehen dieselbe Reinigung.

    Eine Tabelle, die als Buchstabenbrei ankommt, ist schlechter als eine
    Liste, die als Liste ankommt – für Lesende wie für Bildschirmleser. Und
    eine zweite, tabellenfreie Ausgabefassung nur fürs CMS liefe irgendwann
    auseinander. Deshalb gibt es nur diese eine Form, und sie funktioniert an
    beiden Orten.

    ## Wie die beiden Tabellen abgebildet werden

    * **Mit Spaltenbeschriftungen** (Abschnitt 2: „Aussage | Wo Sie es
      nachsehen | Was Sie finden“): je Zeile ein Listenpunkt, der die
      Beschriftung vor den Wert setzt – „Aussage: …“. Ohne die Beschriftungen
      wäre nicht mehr erkennbar, was der zweite Wert bedeutet.
    * **Ohne Spaltenbeschriftungen** (Abschnitt 13, `| |  |`): Das erste Feld
      *ist* die Beschriftung. Es kommt fett voran, der Rest folgt.
    """

    def felder(zeile: str) -> list[str]:
        return [feld.strip() for feld in zeile.strip().strip("|").split("|")]

    kopf = felder(zeilen[0])
    beschriftet = any(feld for feld in kopf)

    ausgabe = ["<ul>"]
    for zeile in zeilen[2:]:  # Zeile 1 ist die Trennzeile |---|---|
        werte = felder(zeile)
        if beschriftet:
            teile = [
                f"<strong>{fliesstext(k)}:</strong> {fliesstext(w)}"
                for k, w in zip(kopf, werte)
                if w
            ]
            ausgabe.append(f"<li>{' — '.join(teile)}</li>")
        elif werte:
            rest = " ".join(fliesstext(w) for w in werte[1:] if w)
            # Die Merkmale sind in der Quelle bereits fett ausgezeichnet
            # (`**Stand**`). Ein zweites <strong> darum ergäbe verschachtelte
            # Auszeichnung – sichtbar folgenlos, aber ein Bildschirmleser mit
            # Betonungsansage sagt es zweimal.
            roh = werte[0]
            beschriftung = fliesstext(roh)
            if not roh.startswith("**"):
                beschriftung = f"<strong>{beschriftung}</strong>"
            ausgabe.append(f"<li>{beschriftung}: {rest}</li>")
    ausgabe.append("</ul>")
    return ausgabe


#: Ein eingerueckter Aufzaehlungspunkt – die zweite Ebene, die dieser Umsetzer
#: nicht kann. Er wuerde sonst in den Punkt darueber hineingezogen und
#: verschwaende als eigener Listenpunkt.
UNTERPUNKT = re.compile(r"^\s+[-*]\s")

#: Dasselbe Muster, aus der Sicht der Fortsetzungszeile: Nur eine Zeile, die
#: KEIN Unterpunkt ist, darf an den vorigen Listenpunkt angehaengt werden.
FORTSETZUNG_KEIN_UNTERPUNKT = UNTERPUNKT

#: Kursivauszeichnung – ein einzelnes Sternchen um ein Wort. Doppelte (fett)
#: bleiben aussen vor; der Umsetzer kennt nur die.
KURSIV = re.compile(r"(?<![*\w])\*(?!\*)[^*]+\*(?![*\w])")


def umsetzen(markdown: str) -> str:
    zeilen = markdown.split("\n")
    ausgabe: list[str] = []
    absatz: list[str] = []
    liste: list[str] = []
    index = 0

    def absatz_schliessen() -> None:
        if absatz:
            ausgabe.append(f"<p>{fliesstext(' '.join(absatz))}</p>")
            absatz.clear()

    def liste_schliessen() -> None:
        if liste:
            # `.extend()` und nicht `+=`: Das Zuweisen machte `ausgabe` zu
            # einer Ortsvariablen dieser inneren Funktion, und der Zugriff
            # schlüge fehl, bevor er etwas anhängen könnte.
            ausgabe.append("<ul>")
            ausgabe.extend(f"<li>{fliesstext(punkt)}</li>" for punkt in liste)
            ausgabe.append("</ul>")
            liste.clear()

    while index < len(zeilen):
        zeile = zeilen[index]
        blank = zeile.strip() == ""

        if zeile.startswith("```"):
            absatz_schliessen()
            liste_schliessen()
            index += 1
            inhalt: list[str] = []
            while index < len(zeilen) and not zeilen[index].startswith("```"):
                inhalt.append(zeilen[index])
                index += 1
            ausgabe.append(f"<pre><code>{maskieren(chr(10).join(inhalt))}</code></pre>")
            index += 1
            continue

        if zeile.startswith("|"):
            absatz_schliessen()
            liste_schliessen()
            block: list[str] = []
            while index < len(zeilen) and zeilen[index].startswith("|"):
                block.append(zeilen[index])
                index += 1
            ausgabe += tabelle(block)
            continue

        if blank:
            absatz_schliessen()
            liste_schliessen()
        elif zeile.startswith("### "):
            absatz_schliessen()
            liste_schliessen()
            text = fliesstext(zeile[4:])
            ausgabe.append(f'<h3 id="{kennung(text)}">{text}</h3>')
        elif zeile.startswith("## "):
            absatz_schliessen()
            liste_schliessen()
            text = fliesstext(zeile[3:])
            ausgabe.append(f'<h2 id="{kennung(text)}">{text}</h2>')
        elif zeile.startswith("# "):
            absatz_schliessen()
            liste_schliessen()
            ausgabe.append(f"<h1>{fliesstext(zeile[2:])}</h1>")
        elif zeile.strip() == "---":
            absatz_schliessen()
            liste_schliessen()
            # Die Trennlinien gliedern die Abschnitte. Als <hr> wären sie eine
            # zweite, tonlose Gliederung neben den Überschriften; Bildschirm-
            # leser lesen sie mit. Die Überschriften tragen die Gliederung
            # bereits, deshalb entfallen sie.
        elif zeile.startswith("- "):
            absatz_schliessen()
            liste.append(zeile[2:].strip())
        elif liste and zeile.startswith("  ") and not FORTSETZUNG_KEIN_UNTERPUNKT.match(zeile):
            liste[-1] += " " + zeile.strip()  # umgebrochener Listenpunkt
        else:
            # Was der Umsetzer nicht kann, muss er melden – so sagt es der
            # Modulkopf zu.
            #
            # Bis Fassung 0.24.1 standen hier nur zwei Formen, und drei
            # weitere fielen still durch. Nachgemessen an `umsetzen()`:
            # „- Punkt\n  - Unterpunkt" zog den Unterpunkt in den
            # Elterneintrag hinein und liess ihn als eigenen Punkt
            # verschwinden; „#### Vierte Ebene" erschien mit den vier Rauten
            # als Text und ohne Ebene in der Gliederung (Barriere nach
            # WCAG 1.3.1); „*kursiv*" zeigte die Sternchen.
            #
            # Die Seite ist die veroeffentlichte Datenschutzerklaerung – dort
            # ist eine stumm verschluckte Auszeichnung schlimmer als ein
            # Abbruch beim Erzeugen.
            for unerwartet, was in (
                ("> ", "Blockzitat"),
                ("* ", "Aufzählung mit *"),
                ("#### ", "Überschrift der vierten Ebene"),
            ):
                if zeile.lstrip().startswith(unerwartet):
                    sys.exit(
                        f"Zeile {index + 1}: {was} gefunden. Diese Auszeichnung "
                        "setzt der Umsetzer nicht um; sie würde stillschweigend "
                        "als Absatz erscheinen. Entweder im Dokument vermeiden "
                        "oder hier ergänzen."
                    )
            if UNTERPUNKT.match(zeile):
                sys.exit(
                    f"Zeile {index + 1}: verschachtelte Aufzählung gefunden. "
                    "Der Umsetzer kennt nur eine Ebene; der Unterpunkt würde "
                    "in den Punkt darüber hineingezogen und verschwände. "
                    "Entweder im Dokument vermeiden oder hier ergänzen."
                )
            if KURSIV.search(zeile):
                sys.exit(
                    f"Zeile {index + 1}: Kursivauszeichnung gefunden. Der "
                    "Umsetzer kennt nur **fett**; die Sternchen erschienen als "
                    "Zeichen auf der Seite. Entweder im Dokument vermeiden "
                    "oder hier ergänzen."
                )
            absatz.append(zeile.strip())
        index += 1

    absatz_schliessen()
    liste_schliessen()
    return "\n".join(ausgabe)


STIL = """
  :root { color-scheme: light dark; }
  body {
    max-width: 46rem; margin: 0 auto; padding: 2rem 1rem;
    font-family: system-ui, sans-serif; line-height: 1.6;
  }
  h1, h2, h3 { line-height: 1.25; }
  h2 { margin-top: 2.5rem; }
  table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
  th, td { border: 1px solid; padding: 0.4rem 0.6rem; text-align: left;
           vertical-align: top; }
  pre { padding: 0.6rem 0.8rem; overflow-x: auto; border: 1px solid; }
  code { font-family: ui-monospace, monospace; }
  a { color: inherit; }
"""


def main() -> None:
    inhalt = umsetzen(veroeffentlichungsteil(QUELLE.read_text(encoding="utf-8")))
    seite = (
        "<!doctype html>\n"
        '<html lang="de">\n'
        "<head>\n"
        '<meta charset="utf-8">\n'
        '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
        f"<title>{TITEL}</title>\n"
        f"<style>{STIL}</style>\n"
        "</head>\n"
        "<body>\n"
        "<!-- ERZEUGT AUS docs/datenschutz.md – NICHT HIER BEARBEITEN.\n"
        "     Neu erzeugen mit: python tools/datenschutz_html.py\n"
        "\n"
        "     Zum Einsetzen in ein Redaktionssystem genügt der Bereich\n"
        "     zwischen den beiden Marken AUSSCHNITT-ANFANG und -ENDE. Er\n"
        "     enthält keine eigene Gestaltung und übernimmt die der Seite. -->\n"
        "<!-- AUSSCHNITT-ANFANG -->\n"
        f"{inhalt}\n"
        "<!-- AUSSCHNITT-ENDE -->\n"
        "</body>\n"
        "</html>\n"
    )
    # `newline="\n"` ausdrücklich: Ohne die Angabe übersetzt Python unter
    # Windows jedes `\n` in ein `\r\n`. `.gitattributes` verlangt aber
    # `* text=auto eol=lf`, und der Arbeitsbaum soll dieselben Zeilenenden
    # tragen wie das Archiv – sonst meldet Prettier eine Datei, an der
    # inhaltlich nichts falsch ist.
    with ZIEL.open("w", encoding="utf-8", newline="\n") as datei:
        datei.write(seite)
    print(f"{ZIEL.relative_to(WURZEL)} geschrieben ({len(seite):,} Zeichen).")


if __name__ == "__main__":
    main()
