"""Viewer-Service: liest .planer/-Dateien eines Projekts in Strukturen für die API.

Reine Bibliothek (kein FastAPI-Import). Eigener toleranter Lese-Parser — gates.py bleibt
unangetastet (scan-kritisch) und verwirft z. B. die Entscheidungs-SÄTZE, die die UI zeigt.
Code je Einheit wird LIVE aus dem Anker extrahiert, nie gespeichert (Single Source).
"""
import re
from pathlib import Path

import artefakte
import repo

_EINHEIT_KOPF = re.compile(r"^## (\S+::\S+|\S+\.\w+)$")


def lese_einheiten(text: str) -> dict[str, dict]:
    """Einheiten-Datei → {id: {beschreibung, input, output, entscheidungen, kanten, anker}}."""
    out: dict[str, dict] = {}
    aktuelle = None
    modus = None
    for zeile in text.splitlines():
        if (m := _EINHEIT_KOPF.match(zeile.strip())) and zeile.startswith("## "):
            aktuelle = {"id": m.group(1), "beschreibung": "", "input": "", "output": "",
                        "entscheidungen": [], "kanten": [], "anker": []}
            out[aktuelle["id"]] = aktuelle
            modus = None
        elif aktuelle is None:
            continue
        elif m := re.match(r"(beschreibung|input|output):(.*)$", zeile):
            aktuelle[m.group(1)] = m.group(2).strip()
            modus = m.group(1)
        elif zeile.startswith("anker:"):
            aktuelle["anker"].append(zeile.split(":", 1)[1].strip())
        elif zeile.strip() in ("entscheidungen:", "fakten:"):
            modus = "punkte"
        elif zeile.strip() == "kanten:":
            modus = "kanten"
        elif modus == "punkte" and re.match(r"\s+beleg:", zeile):
            continue  # UI zeigt echten Code statt Zitaten
        elif modus == "punkte" and zeile.startswith("- "):
            aktuelle["entscheidungen"].append(re.sub(r"\s*\bbeleg:.*$", "", zeile[2:]).strip())
        elif modus == "punkte" and zeile.startswith("  ") and aktuelle["entscheidungen"]:
            aktuelle["entscheidungen"][-1] += " " + zeile.strip()
        elif modus == "kanten" and (m := re.match(r"- ([\wäöü-]+):\s*(\S+)", zeile)):
            aktuelle["kanten"].append({"typ": m.group(1), "ziel": m.group(2)})
        elif modus in ("beschreibung", "input", "output") and zeile.startswith("  "):
            aktuelle[modus] += " " + zeile.strip()
    return out


def lese_kern(text: str) -> dict:
    zeilen = text.splitlines()
    titel = zeilen[0].removeprefix("# Kern:").strip() if zeilen else ""
    tragend: list[str] = []
    rumpf: list[str] = []
    for zeile in zeilen[1:]:
        if zeile.startswith("tragende-einheiten:"):
            tragend = [t.strip() for t in zeile.split(":", 1)[1].split(",") if t.strip()]
        else:
            rumpf.append(zeile)
    return {"titel": titel, "text": "\n".join(rumpf).strip(), "tragende_einheiten": tragend}


def _einheiten_refs(zeile: str) -> list[str]:
    return [t.strip() for t in zeile.split(":", 1)[1].split(",") if t.strip()]


def lese_features(text: str) -> dict:
    """Eine Bereichs-Datei → {name, beschreibung, features: [{titel, flag, beschreibung,
    teilfeatures: [{titel, einheiten}]}]}."""
    bereich = {"name": "", "beschreibung": "", "features": []}
    feature = None
    for zeile in text.splitlines():
        if zeile.startswith("# Bereich:"):
            bereich["name"] = zeile.split(":", 1)[1].strip()
            feature = None
        elif zeile.startswith("## Feature:"):
            titel = zeile[11:].strip()
            flag = "kern" if "[kern]" in titel else "rand"
            feature = {"titel": re.sub(r"\s*\[(kern|rand)\]", "", titel).strip(),
                       "flag": flag, "beschreibung": "", "teilfeatures": []}
            bereich["features"].append(feature)
        elif zeile.startswith("### ") and feature is not None:
            feature["teilfeatures"].append({"titel": zeile[4:].strip(), "einheiten": []})
        elif zeile.startswith("einheiten:") and feature and feature["teilfeatures"]:
            feature["teilfeatures"][-1]["einheiten"] = _einheiten_refs(zeile)
        elif zeile.startswith("beschreibung:"):
            ziel = feature if feature is not None else bereich
            ziel["beschreibung"] = zeile.split(":", 1)[1].strip()
    return bereich


def lese_architektur(text: str) -> list[dict]:
    komponenten: list[dict] = []
    k = None
    modus = None
    for zeile in text.splitlines():
        if zeile.startswith("## Komponente:"):
            k = {"name": zeile.split(":", 1)[1].strip(), "beschreibung": "",
                 "einheiten": [], "beziehungen": []}
            komponenten.append(k)
            modus = None
        elif k is None:
            continue
        elif zeile.startswith("beschreibung:"):
            k["beschreibung"] = zeile.split(":", 1)[1].strip()
        elif zeile.startswith("einheiten:"):
            k["einheiten"] = _einheiten_refs(zeile)
        elif zeile.strip() == "beziehungen:":
            modus = "beziehungen"
        elif modus == "beziehungen" and (m := re.match(r"- ([\wäöü-]+):\s*(.+)$", zeile)):
            k["beziehungen"].append({"typ": m.group(1), "ziel": m.group(2).strip()})
    return komponenten


def lese_flows(text: str) -> list[dict]:
    flows: list[dict] = []
    f = None
    for zeile in text.splitlines():
        if zeile.startswith("## Flow:"):
            f = {"titel": zeile.split(":", 1)[1].strip(), "schritte": []}
            flows.append(f)
        elif f is not None and (m := re.match(r"^\d+\.\s(.+?)(?:\s—\s(.+))?$", zeile)):
            refs = [t.strip() for t in (m.group(2) or "").split(",") if t.strip()]
            f["schritte"].append({"text": m.group(1).strip(), "einheiten": refs})
    return flows


def lese_pruefung(text: str) -> dict:
    out = {"tests": "", "lint": "", "start": "", "hinweise": []}
    modus = None
    for zeile in text.splitlines():
        if m := re.match(r"(tests|lint|start):(.*)$", zeile):
            out[m.group(1)] = m.group(2).strip()
            modus = None
        elif zeile.strip() == "hinweise:":
            modus = "hinweise"
        elif modus == "hinweise" and zeile.startswith("- "):
            out["hinweise"].append(zeile[2:].strip())
    return out


def code_extrahieren(wurzel: Path, eid: str) -> str:
    """Quellcode eines Ankers (pfad::symbol) LIVE aus der Quelle — nie Exception:
    fehlende Datei/Symbol → '' (UI zeigt Hinweis)."""
    try:
        datei, _, symbol = eid.partition("::")
        p = wurzel / datei
        if not p.is_file():
            return ""
        text = p.read_text(encoding="utf-8", errors="replace")
        if not symbol:
            return text[:8000]  # Datei-Einheit: ganze (gekappte) Datei
        if "." in symbol:
            symbol = symbol.split(".", 1)[1]  # qualifizierte Methode → Methode suchen
        zeilen = text.splitlines()
        start = None
        for i, z in enumerate(zeilen):
            if re.match(rf"(\s*)(?:async\s+)?(?:def|class)\s+{re.escape(symbol)}\b", z):
                start = i
                break
            if re.match(rf"{re.escape(symbol)}\s*[:=]", z):
                start = i
                ende = i + 1
                while ende < len(zeilen) and (zeilen[ende].startswith((" ", "\t", ")", "]", "}"))
                                              or not zeilen[ende].strip()):
                    ende += 1
                return "\n".join(zeilen[start:ende]).rstrip()
        if start is None:
            return ""
        einzug = len(zeilen[start]) - len(zeilen[start].lstrip())
        ende = start + 1
        while ende < len(zeilen):
            z = zeilen[ende]
            if z.strip() and (len(z) - len(z.lstrip())) <= einzug:
                break
            ende += 1
        return "\n".join(zeilen[start:ende]).rstrip()
    except Exception:
        return ""


def lade_projekt(wurzel: Path) -> dict:
    """Alle .planer/-Dateien eines Projekts → Struktur für die API. Fehlende Dateien
    ergeben leere Teile (degradieren, nie werfen)."""
    planer = wurzel / ".planer"

    def text(rel: str) -> str:
        p = planer / rel
        return p.read_text(encoding="utf-8") if p.is_file() else ""

    einheiten: dict[str, dict] = {}
    staende: dict[str, str] = {}  # quelldatei → stand
    for p in sorted((planer / "einheiten").glob("*.md")) if (planer / "einheiten").is_dir() else []:
        inhalt = p.read_text(encoding="utf-8")
        einheiten.update(lese_einheiten(inhalt))
        kopf = inhalt.splitlines()[0] if inhalt else ""
        datei = kopf.removeprefix("# Einheiten:").strip()
        if datei and (stand := artefakte.lese_stand(inhalt)):
            staende[datei] = stand
    bereiche = [lese_features((planer / "features" / p.name).read_text(encoding="utf-8"))
                for p in sorted((planer / "features").glob("*.md"))] \
        if (planer / "features").is_dir() else []
    return {
        "kern": lese_kern(text("kern.md")),
        "bereiche": bereiche,
        "architektur": lese_architektur(text("architektur.md")),
        "flows": lese_flows(text("flows.md")),
        "pruefung": lese_pruefung(text("pruefung.md")),
        "einheiten": einheiten,
        "staende": staende,
    }


# --- Drift ---------------------------------------------------------------------------

_diff_cache: dict[tuple[str, str, str], set[str]] = {}  # (pfad, stand, head) → geändert


def drift(wurzel: Path, staende: dict[str, str]) -> dict:
    """Welche gescannten Quelldateien sind seit ihrem stand: berührt?
    → {head, veraltet: [quelldatei]}. Cache-Schlüssel enthält HEAD → selbst-invalidierend."""
    try:
        head = repo.head_hash(wurzel)
    except (RuntimeError, FileNotFoundError):
        return {"head": "", "veraltet": []}
    veraltet = []
    for datei, stand in sorted(staende.items()):
        if stand == head:
            continue
        schluessel = (str(wurzel), stand, head)
        if schluessel not in _diff_cache:
            try:
                _diff_cache[schluessel] = repo.geaenderte_dateien(wurzel, stand)[0]
            except RuntimeError:
                _diff_cache[schluessel] = set()
        if datei in _diff_cache[schluessel]:
            veraltet.append(datei)
    return {"head": head, "veraltet": veraltet}


# --- Doku-Leseordnung ------------------------------------------------------------------

def doku_ordnung(projekt: dict) -> list[str]:
    """Bereichs-Namen in Abhängigkeitsordnung: Kanten der Einheiten (ruft-auf/nutzt) werden
    zu Bereichs-Kanten aggregiert; viel Genutztes kommt früher (Kahn-Toposort, zyklenfest:
    bei Zyklus gewinnt der Bereich mit kleinstem In-Degree, dann alphabetisch)."""
    einheit_zu_bereich: dict[str, str] = {}
    for bereich in projekt["bereiche"]:
        for f in bereich["features"]:
            for tf in f["teilfeatures"]:
                for eid in tf["einheiten"]:
                    einheit_zu_bereich.setdefault(eid, bereich["name"])
    namen = [b["name"] for b in projekt["bereiche"]]
    eingehend: dict[str, set[str]] = {n: set() for n in namen}
    for eid, e in projekt["einheiten"].items():
        von = einheit_zu_bereich.get(eid)
        for kante in e["kanten"]:
            if kante["typ"] not in ("ruft-auf", "nutzt"):
                continue
            nach = einheit_zu_bereich.get(kante["ziel"])
            if von and nach and von != nach:
                eingehend[nach].add(von)  # "von nutzt nach" → nach ist Fundament → früher
    # Kahn: Bereiche ohne offene Nutzer-Abhängigkeit zuerst — Fundament vor Nutzern:
    # ein Bereich kommt, sobald alles, was ER nutzt, schon gelesen wurde.
    nutzt: dict[str, set[str]] = {n: set() for n in namen}
    for nach, nutzer in eingehend.items():
        for von in nutzer:
            nutzt[von].add(nach)
    ordnung: list[str] = []
    offen = set(namen)
    while offen:
        bereit = sorted(n for n in offen if not (nutzt[n] & offen))
        if not bereit:  # Zyklus: kleinste offene Abhängigkeitszahl, dann Name
            bereit = [min(offen, key=lambda n: (len(nutzt[n] & offen), n))]
        for n in bereit:
            ordnung.append(n)
            offen.discard(n)
    return ordnung
