"""OnePager-Pipeline als Konsens-Kette (7 Karten im 3×3-Raster).

Recherche: 3 Agenten (min. 2, Grace) → Mapping konsolidiert zu EINER Faktenbasis.
Bauen: 3 Agenten bauen je einen Karten-Satz → ein Judge wählt und kombiniert.
Prüfung: Verify→Fix-Loop (max. KONSENS_MAX_RUNDEN); Runde 1 ist fatal, danach
bleibt bei Fehlern die letzte gültige Version. Schritt-Dateien bleiben liegen →
Abbruch erhält Fortschritt, ▶ setzt am offenen Schritt fort.
"""

from pathlib import Path

from config import KONSENS_GRACE, KONSENS_MAX_RUNDEN
from jsonio import read_json_file as _json_datei
from pipeline import (
    CANCELLED, FAILED, GenContext, _extra, _fail, _log, _probleme_schema,
    _prompt, _race, _set_step, _timeout, is_guide_cancelled, run_single_slot,
)

ONEPAGER_STEPS = ("Recherche", "Bauen", "Prüfung")


async def _generate_onepager(
    guide_id: str, topic: str, instructions: str, provider: str,
    project: Path | None, content_path: Path,
) -> list[dict] | None:
    is_cancelled = lambda: is_guide_cancelled(guide_id)
    ctx = GenContext(topic=topic, provider=provider, is_cancelled=is_cancelled, guide_id=guide_id)

    # 3×3-Raster: 7 Karten mit festen Schlüsseln (Reihenfolge = Lesereihenfolge mobil)
    KARTEN_KEYS = ("info", "eigenschaften", "beispiel", "zusammenhaenge", "voraussetzungen", "modern", "veraltet")

    def karten_schema(data):
        """{"karten": {key: {titel, md}}} → Liste · sonst None."""
        if not isinstance(data, dict):
            return None
        karten = data.get("karten")
        if not isinstance(karten, dict):
            return None
        out = []
        for key in KARTEN_KEYS:
            k = karten.get(key)
            if not isinstance(k, dict) or not isinstance(k.get("titel"), str) or not isinstance(k.get("md"), str):
                return None
            titel, md = k["titel"].strip(), k["md"].strip()
            if not titel or len(md) < 5:  # abgebrochene/leere Karten sind ungültig
                return None
            out.append({"key": key, "titel": titel, "md": md})
        return out

    d, stem = content_path.parent, content_path.stem
    recherche_slots = [d / f"{stem}.recherche-{i}.md" for i in (1, 2, 3)]
    recherche_path = d / f"{stem}.recherche.md"   # konsolidierte Faktenbasis
    karten_slots = [d / f"{stem}.karten-{i}.json" for i in (1, 2, 3)]
    karten_path = d / f"{stem}.karten.json"        # Judge-Ausgabe
    verify_paths = {n: d / f"{stem}.verify-r{n}.json" for n in range(1, KONSENS_MAX_RUNDEN + 1)}
    fix_paths = {n: d / f"{stem}.karten-fix-r{n}.json" for n in range(1, KONSENS_MAX_RUNDEN + 1)}

    # Projekte bekommen eigene Recherche-Dimensionen — Produkt-Fragen
    # (Version, Lizenz, Alternativen) laufen dort ins Leere.
    if project:
        source = _prompt("OnePager-Quelle-Projekt", project=project)
        recherche_template = "OnePager-Recherche-Projekt"
    else:
        source = _prompt("OnePager-Quelle-Thema", topic=topic)
        recherche_template = "OnePager-Recherche"

    def text_payload(path: Path):
        if not path.exists():
            return None
        text = path.read_text(encoding="utf-8").strip()
        return text or None

    # Schritt 0: Recherche — 3 Agenten (min. 2, Grace), Mapping konsolidiert.
    # Eine gültige recherche.md (auch aus Altläufen) überspringt den Schritt.
    recherche = text_payload(recherche_path)
    if recherche is None:
        await _set_step(guide_id, 0, "Recherchiere (3 Agenten)…")
        recherchen = []
        offen = []
        for i, path in enumerate(recherche_slots, 1):
            text = text_payload(path)
            if text is not None:
                recherchen.append(text)
            else:
                offen.append((i, path))
        if len(recherchen) < 2:
            slots = [
                {
                    "key": f"{guide_id}-recherche-{i}",
                    "prompt": _prompt(recherche_template, topic=topic, source=source, out_path=path, extra=_extra(instructions)),
                    "role": "quick", "capabilities": "files" if project else "full",
                    "payload": (lambda result, p=path: text_payload(p)),
                }
                for i, path in offen
            ]
            neue = await _race(
                topic, "OnePager-Recherche", slots, 2 - len(recherchen),
                _timeout("onepager_recherche"), provider, cancelled=is_cancelled, grace=KONSENS_GRACE,
            )
            if is_cancelled():
                return None
            if neue is None:
                await _fail(guide_id, "OnePager-Recherche fehlgeschlagen (Minimum nicht erreicht)")
                return None
            recherchen += neue

        await _set_step(guide_id, 0, "Konsolidiere Recherche…")
        recherchen_block = "\n\n".join(f"### Recherche {i}\n\n{text}" for i, text in enumerate(recherchen, 1))
        status, recherche = await run_single_slot(
            ctx, "Recherche-Mapping",
            key=f"{guide_id}-recherche-mapping",
            prompt=_prompt(
                "OnePager-Recherche-Mapping",
                topic=topic, n=len(recherchen), recherchen=recherchen_block, out_path=recherche_path,
            ),
            role="judge", capabilities="files",
            payload=lambda result: text_payload(recherche_path),
            timeout=_timeout("onepager_mapping"),
        )
        if status == CANCELLED:
            return None
        if status == FAILED:
            await _fail(guide_id, "Recherche-Konsolidierung fehlgeschlagen")
            return None

    # Schritt 1: Bauen — 3 Entwürfe (min. 2, Grace), ein Judge kombiniert.
    # Gültiges karten.json (auch aus Altläufen) überspringt den Schritt.
    karten = karten_schema(_json_datei(karten_path))
    if karten is None:
        await _set_step(guide_id, 1, "Baue OnePager (3 Entwürfe)…")
        entwuerfe = []
        offen = []
        for i, path in enumerate(karten_slots, 1):
            res = karten_schema(_json_datei(path))
            if res is not None:
                entwuerfe.append(res)
            else:
                offen.append((i, path))
        if len(entwuerfe) < 2:
            slots = [
                {
                    "key": f"{guide_id}-bauen-{i}",
                    "prompt": _prompt("OnePager-Bauen", topic=topic, recherche=recherche, out_path=path, extra=_extra(instructions)),
                    "role": "fast", "capabilities": "files",
                    "payload": (lambda result, p=path: karten_schema(_json_datei(p))),
                }
                for i, path in offen
            ]
            neue = await _race(
                topic, "OnePager-Bauen", slots, 2 - len(entwuerfe),
                _timeout("onepager_bauen"), provider, cancelled=is_cancelled, grace=KONSENS_GRACE,
            )
            if is_cancelled():
                return None
            if neue is None:
                await _fail(guide_id, "OnePager-Bau fehlgeschlagen (Minimum nicht erreicht)")
                return None
            entwuerfe += neue

        await _set_step(guide_id, 1, "Wähle besten Entwurf…")
        saetze_block = "\n\n".join(
            f"## Entwurf {i}\n\n" + "\n\n".join(f"### {k['titel']} [{k['key']}]\n{k['md']}" for k in satz)
            for i, satz in enumerate(entwuerfe, 1)
        )
        status, karten = await run_single_slot(
            ctx, "Bauen-Judge",
            key=f"{guide_id}-bauen-judge",
            prompt=_prompt(
                "OnePager-Bauen-Judge",
                topic=topic, n=len(entwuerfe), recherche=recherche, kartensaetze=saetze_block,
                out_path=karten_path, extra=_extra(instructions),
            ),
            role="judge", capabilities="files",
            payload=lambda result: karten_schema(_json_datei(karten_path)),
            timeout=_timeout("onepager_judge"),
        )
        if status == CANCELLED:
            return None
        if status == FAILED:
            await _fail(guide_id, "OnePager-Bau fehlgeschlagen (Judge ohne gültiges Ergebnis)")
            return None

    def karten_block() -> str:
        return "\n\n".join(f"### {k['titel']} [{k['key']}]\n{k['md']}" for k in karten)

    # Schritt 2: Prüf-Loop — Verify notiert Probleme, Fix behebt; max. Runden-Cap.
    # Runde 1 ist fatal (wie früher der Einzel-Check), danach bleibt bei Fehlern
    # die letzte gültige Version stehen.
    for runde in range(1, KONSENS_MAX_RUNDEN + 1):
        probleme = _probleme_schema(_json_datei(verify_paths[runde]))
        if probleme is None:
            await _set_step(guide_id, 2, f"Prüfe OnePager (Runde {runde}/{KONSENS_MAX_RUNDEN})…")
            verify_paths[runde].unlink(missing_ok=True)
            status, probleme = await run_single_slot(
                ctx, f"OnePager-Prüfung r{runde}",
                key=f"{guide_id}-verify-r{runde}",
                prompt=_prompt("OnePager-Verifikation", topic=topic, recherche=recherche, karten=karten_block(), out_path=verify_paths[runde]),
                role="judge", capabilities="files",
                payload=lambda result, p=verify_paths[runde]: _probleme_schema(_json_datei(p)),
                timeout=_timeout("onepager_verify"),
            )
            if status == CANCELLED:
                return None
            if status == FAILED:
                if runde == 1:
                    await _fail(guide_id, "OnePager-Prüfung fehlgeschlagen")
                    return None
                _log(topic, f"OnePager-Prüfung Runde {runde} fehlgeschlagen — letzte gültige Version bleibt")
                break
        if not probleme:
            break
        if runde == KONSENS_MAX_RUNDEN:
            _log(topic, f"OnePager-Prüfung: {len(probleme)} Problem(e) bleiben nach Runde {runde} stehen")
            break

        _log(topic, f"OnePager-Prüfung Runde {runde}: {len(probleme)} Problem(e) notiert")
        fixed = karten_schema(_json_datei(fix_paths[runde]))  # Resume
        if fixed is None:
            await _set_step(guide_id, 2, f"Überarbeite OnePager (Runde {runde})…")
            status, fixed = await run_single_slot(
                ctx, f"OnePager-Fix r{runde}",
                key=f"{guide_id}-karten-fix-r{runde}",
                prompt=_prompt(
                    "OnePager-Fix",
                    topic=topic, recherche=recherche, karten=karten_block(),
                    probleme="\n".join(f"- {p}" for p in probleme),
                    out_path=fix_paths[runde], extra=_extra(instructions),
                ),
                role="fast", capabilities="files",
                payload=lambda result, p=fix_paths[runde]: karten_schema(_json_datei(p)),
                timeout=_timeout("onepager_bauen"),
            )
            if status == CANCELLED:
                return None
            if status == FAILED:
                _log(topic, f"OnePager-Fix Runde {runde} ungültig — letzte gültige Version bleibt")
                break
        karten = fixed

    sections = [
        {"num": i, "title": k["titel"], "md": k["md"], "key": k["key"]}
        for i, k in enumerate(karten, 1)
    ]
    return [{"title": topic, "sections": sections}]
