#!/usr/bin/env python3
"""Mechanische Prüfung einer Einheiten-Datei (Phase 0).

Aufruf: pruefe.py <einheiten.md> <projekt-wurzel>

Prüft tokenfrei:
1. Anker-Existenz  — jede Einheiten-ID und jedes Kanten-Ziel zeigt auf existierende
                     Datei + darin definiertes Symbol (def/class/Zuweisung).
2. Zitat-Treue     — jeder beleg:-String kommt wörtlich in der Anker-Datei vor.
3. Struktur        — Schlüssel-Grammatik (beschreibung/fakten/kanten, Kanten-Typen).
4. Abdeckung       — Report: welche Top-Level-Symbole der Quelldatei keiner Einheit
                     zugeordnet sind (v1 kein Fehler).
"""
import re
import sys
from pathlib import Path

KANTEN_TYPEN = {"ruft-auf", "nutzt", "wird-genutzt-von", "löst-aus", "wird-ausgelöst-von"}


def entschaerfe(s: str) -> str:
    """Escaping-Rauschen der Modelle normalisieren: \\" → " und \\\\ → \\."""
    return s.replace('\\"', '"').replace("\\\\", "\\")


def symbole_in(quelle: str) -> set[str]:
    """Top-Level-Symbole: def/class/Konstanten-Zuweisung auf Spalte 0 (+ async def)."""
    out = set()
    for m in re.finditer(r"^(?:async\s+)?(?:def|class)\s+(\w+)", quelle, re.M):
        out.add(m.group(1))
    for m in re.finditer(r"^(\w+)\s*[:=]", quelle, re.M):
        out.add(m.group(1))
    return out


def main(pfad: str, wurzel: str) -> int:
    text = Path(pfad).read_text(encoding="utf-8")
    wurzel_p = Path(wurzel)
    fehler, warnungen = [], []
    quell_cache: dict[str, str | None] = {}

    def quelle(datei: str) -> str | None:
        if datei not in quell_cache:
            p = wurzel_p / datei
            quell_cache[datei] = p.read_text(encoding="utf-8") if p.is_file() else None
        return quell_cache[datei]

    def pruefe_id(eid: str, kontext: str):
        if "::" not in eid:
            if quelle(eid) is None:
                fehler.append(f"{kontext}: Datei fehlt: {eid}")
            return
        datei, symbol = eid.split("::", 1)
        q = quelle(datei)
        if q is None:
            fehler.append(f"{kontext}: Datei fehlt: {datei}")
        elif symbol not in symbole_in(q):
            fehler.append(f"{kontext}: Symbol '{symbol}' nicht top-level in {datei}")

    # --- parsen ---
    einheiten: list[dict] = []
    zusatz_anker: set[str] = set()
    aktuelle = None
    modus = None  # None | fakten | kanten
    for nr, zeile in enumerate(text.splitlines(), 1):
        if zeile.startswith("## "):
            aktuelle = {"id": zeile[3:].strip(), "zeile": nr, "belege": [], "kanten": [],
                        "beschreibung": False}
            einheiten.append(aktuelle)
            modus = None
        elif zeile.startswith(("anker:", "ergaenze-anker:")):
            # Zusatz-Anker dürfen auch verschachtelte Symbole sein (Innenfunktionen);
            # ergaenze-anker kommt auch außerhalb von Einheiten vor (Nachfass-Ausgabe)
            aid = zeile.split(":", 1)[1].strip().split(" zu ")[0].strip()
            zusatz_anker.add(aid)
            if "::" in aid:
                datei, symbol = aid.split("::", 1)
                q = quelle(datei)
                if q is None:
                    fehler.append(f"Z{nr} anker: Datei fehlt: {datei}")
                elif not re.search(rf"(?:def|class)\s+{re.escape(symbol)}\b|^{re.escape(symbol)}\s*[:=]",
                                   q, re.M):
                    fehler.append(f"Z{nr} anker: Symbol '{symbol}' nicht in {datei}")
            else:
                pruefe_id(aid, f"Z{nr} anker")
        elif aktuelle is None:
            continue
        elif zeile.startswith("beschreibung:"):
            aktuelle["beschreibung"] = True
            modus = None
        elif zeile.strip() in ("fakten:", "entscheidungen:"):
            modus = "fakten"
        elif zeile.startswith("input:"):
            aktuelle["input"] = True
        elif zeile.startswith("output:"):
            aktuelle["output"] = True
        elif zeile.strip() == "kanten:":
            modus = "kanten"
        elif m := re.match(r'\s+beleg:\s*"(.*)"\s*$', zeile):
            aktuelle["belege"].append((nr, entschaerfe(m.group(1))))
        elif modus == "fakten" and (m := re.search(r'\bbeleg:\s*"(.*)"\s*$', zeile)):
            # tolerant: beleg inline am Ende der Fakt-Zeile (streng schreiben, tolerant parsen)
            aktuelle["belege"].append((nr, entschaerfe(m.group(1))))
        elif modus == "kanten" and (m := re.match(r"- ([\wäöü-]+):\s*(\S+)\s*$", zeile)):
            aktuelle["kanten"].append((nr, m.group(1), m.group(2)))
        elif modus == "kanten" and zeile.startswith("- "):
            fehler.append(f"Z{nr}: Kante unlesbar (Zusatztext?): {zeile.strip()!r}")

    # --- prüfen ---
    for e in einheiten:
        kontext = e["id"]
        pruefe_id(e["id"], f"Z{e['zeile']} einheit")
        if not e["beschreibung"]:
            fehler.append(f"{kontext}: beschreibung: fehlt")
        datei = e["id"].split("::")[0]
        q = quelle(datei) or ""
        for nr, beleg in e["belege"]:
            if beleg not in q:
                fehler.append(f"{kontext} Z{nr}: Zitat nicht wörtlich in {datei}: \"{beleg[:50]}…\"")
        if not e["belege"]:
            warnungen.append(f"{kontext}: keine Belege")
        if not e.get("input") or not e.get("output"):
            warnungen.append(f"{kontext}: input:/output: fehlt")
        for nr, typ, ziel in e["kanten"]:
            if typ not in KANTEN_TYPEN:
                fehler.append(f"{kontext} Z{nr}: unbekannter Kanten-Typ '{typ}'")
            pruefe_id(ziel, f"{kontext} Z{nr} kante")

    # --- Abdeckung der gescannten Datei ---
    dateien = {e["id"].split("::")[0] for e in einheiten if "::" in e["id"]}
    for datei in sorted(dateien):
        q = quelle(datei)
        if q is None:
            continue
        zugeordnet = {e["id"].split("::", 1)[1] for e in einheiten
                      if e["id"].startswith(datei + "::")}
        zugeordnet |= {a.split("::", 1)[1] for a in zusatz_anker
                       if a.startswith(datei + "::")}
        oeffentlich = {s for s in symbole_in(q)
                       if re.search(rf"^(?:async\s+)?(?:def|class)\s+{re.escape(s)}\b", q, re.M)}
        fehlt = oeffentlich - zugeordnet
        if fehlt:
            warnungen.append(f"Abdeckung {datei}: nicht zugeordnet: {', '.join(sorted(fehlt))}")

    print(f"Einheiten: {len(einheiten)}, Belege: {sum(len(e['belege']) for e in einheiten)}, "
          f"Kanten: {sum(len(e['kanten']) for e in einheiten)}")
    for f in fehler:
        print(f"FEHLER  {f}")
    for w in warnungen:
        print(f"WARNUNG {w}")
    print(f"\n{len(fehler)} Fehler, {len(warnungen)} Warnungen")
    return 1 if fehler else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1], sys.argv[2]))
