"""
Ideen-Queue — DIE eine Sammelstelle für Ideen des Commanders (Entscheid 10.07.2026).

Die Queue selbst ist das NATIVE Hermes-Kanban (SQLite-Board, Dispatcher im Gateway):
Idee rein (triage) → der Specifier arbeitet sie aus → ein Worker-Profil setzt sie um →
Code-Ergebnisse kommen als Vorschlags-Branch zurück und erscheinen als Karte im
Auftragsbuch. Dieser Service ist nur die MC2-Tür dazu: anlegen + anzeigen + aufräumen,
alles über die Hermes-CLI (stabile --json-Schnittstelle, kein Griff in die kanban.db).

Türen insgesamt: Telegram/Desktop (natives kanban_create-Tool), Lucy-Voice
(mcp_voice.idee_notieren → POST /api/ideen) und die Zentrale (Auftragsbuch-Tab → hier).

Lokal (Windows-Dev) ist alles harmlos: available=False, Aktionen geben Fehler statt zu crashen.
"""

import json
import logging
import os
import re
import sqlite3
import subprocess
import time
from pathlib import Path

log = logging.getLogger(__name__)

HERMES = Path(os.environ.get("MC_HERMES_BIN", "~/.local/bin/hermes")).expanduser()
# Ketten-Sicht (20.07.): Eltern-Links + Heartbeat-Notizen stehen NICHT im `list --json`
# der CLI — dafür (und NUR dafür) ein read-only-Blick in die kanban.db. Das bricht die
# „alles über die CLI"-Regel bewusst minimal: zwei stabile Tabellen (task_links 2 Spalten,
# task_events), Verbindung strikt mode=ro, jeder Fehler fällt lautlos auf die flache
# Sicht zurück. Schreiben läuft weiterhin ausschließlich über die CLI.
KANBAN_DB = Path(os.environ.get("MC_KANBAN_DB", "~/.hermes/kanban.db")).expanduser()

_TASK_ID_RX = re.compile(r"^t_[0-9a-f]{4,16}$")

# Die CLI kostet pro Aufruf ein paar Sekunden Python-Start — die UI pollt aber.
_list_cache: dict = {"ts": 0.0, "data": None}
_LIST_EVERY = 15.0  # s

# Cache für worker log
_log_cache: dict = {"ts": 0.0, "task_id": None, "data": None}
_LOG_EVERY = 4.0  # s

# Cache für Ergebnis (Abschluss-Zusammenfassung) — ändert sich nach done nie mehr
_erg_cache: dict = {}
_ERG_MAX = 40  # simple Deckelung gegen unbegrenztes Wachsen


def _available() -> bool:
    return os.name == "posix" and HERMES.is_file()


def _hermes(args: list[str], timeout: int = 60) -> subprocess.CompletedProcess:
    return subprocess.run([str(HERMES), "kanban", *args],
                          capture_output=True, text=True, timeout=timeout)


def _parse_json(stdout: str):
    """CLI-Ausgabe kann Log-Zeilen vor dem JSON enthalten — ab der ersten Klammer parsen."""
    text = stdout or ""
    for opener in ("[", "{"):
        idx = text.find(opener)
        if idx >= 0:
            try:
                return json.loads(text[idx:])
            except json.JSONDecodeError:
                continue
    return None


def _blocker_frage(task_id: str) -> dict | None:
    """Für eine blockierte Aufgabe: die Frage/den Grund des Workers holen (needs_input etc.).

    Die Wahrheit steckt im jüngsten `blocked`-Event (payload.reason/kind); fällt das leer aus,
    greifen wir auf den letzten „BLOCKED:"-Kommentar zurück. Ein Extra-`show`-Aufruf pro
    hängender Karte ist billig — die gibt es fast nie und wenn, dann nur eine Handvoll.
    """
    try:
        r = _hermes(["show", task_id, "--json"], timeout=30)
        d = _parse_json(r.stdout)
    except Exception:
        log.warning("ideen: kanban show %s fehlgeschlagen", task_id, exc_info=True)
        return None
    if not isinstance(d, dict):
        return None

    frage, kind = "", ""
    for ev in d.get("events") or []:          # chronologisch → letztes blocked = aktive Frage
        if isinstance(ev, dict) and ev.get("kind") == "blocked":
            p = ev.get("payload") or {}
            frage = (p.get("reason") or "").strip()
            kind = (p.get("kind") or "").strip()
    if not frage:                             # Fallback: letzter „BLOCKED:"-Kommentar
        for c in d.get("comments") or []:
            body = (c.get("body") or "").strip()
            if body.startswith("BLOCKED:"):
                frage = body[len("BLOCKED:"):].strip()
    return {"frage": frage[:800], "kind": kind}


def _projekt_titel(titles: list[str]) -> str:
    """Anzeigename einer Karten-Familie: gemeinsamer Titel-Präfix (z. B. „Homelab-Dashboard 2.0"),
    sonst der Titel der ältesten Karte. Reine Anzeige-Heuristik, nichts hängt davon ab."""
    if not titles:
        return "Projekt"
    prefix = titles[0]
    for t in titles[1:]:
        while prefix and not t.startswith(prefix):
            prefix = prefix[:-1]
    prefix = prefix.strip(" –-:·")
    return prefix if len(prefix) >= 10 else titles[0][:80]


def _ketten_anreichern(items: list[dict]) -> list[dict]:
    """Ketten-Sicht über die Queue legen (read-only, best-effort).

    Ergänzt pro Karte: `eltern` (offene Vorgänger-IDs), `wartet_auf` (deren Titel),
    `notiz` (jüngste Heartbeat-Notiz des Workers bei running) und `projekt`
    (Familien-Schlüssel). Liefert die Projekt-Gruppen für die UI: zusammenhängende
    Karten (task_links) = ein Projekt; Einzelkarten bleiben ohne `projekt`.
    Jeder Fehler (Windows-Dev, Schema-Drift, Lock) → unveränderte flache Sicht.
    """
    for it in items:
        it.setdefault("eltern", [])
        it.setdefault("wartet_auf", [])
        it.setdefault("notiz", None)
        it.setdefault("projekt", None)
    if not items or not KANBAN_DB.is_file():
        return []
    by_id = {it["id"]: it for it in items if it.get("id")}
    try:
        con = sqlite3.connect(f"file:{KANBAN_DB}?mode=ro", uri=True, timeout=3)
        try:
            con.execute("PRAGMA busy_timeout=2000")
            links = [(p, c) for p, c in con.execute(
                "SELECT parent_id, child_id FROM task_links")
                if p in by_id or c in by_id]
            for it in items:
                if it.get("status") != "running":
                    continue
                for (payload,) in con.execute(
                        "SELECT payload FROM task_events WHERE task_id=? AND kind='heartbeat' "
                        "AND payload IS NOT NULL ORDER BY created_at DESC LIMIT 5", (it["id"],)):
                    try:
                        note = (json.loads(payload) or {}).get("note")
                    except Exception:
                        note = None
                    if note:
                        it["notiz"] = str(note)[:300]
                        break
        finally:
            con.close()
    except Exception:
        log.debug("ideen: Ketten-Anreicherung fehlgeschlagen", exc_info=True)
        return []

    # Eltern/wartet_auf: nur Vorgänger zählen, die selbst noch offen in der Liste stehen —
    # archivierte/fremde Parents gelten als erledigt.
    ERLEDIGT = ("done",)
    for parent, child in links:
        it = by_id.get(child)
        p = by_id.get(parent)
        if it and p and p.get("status") not in ERLEDIGT:
            it["eltern"].append(parent)
            if len(it["wartet_auf"]) < 3:
                it["wartet_auf"].append((p.get("titel") or parent)[:90])

    # Familien = zusammenhängende Komponenten über die Links (Union-Find, klein genug).
    chef: dict[str, str] = {}

    def boss(x: str) -> str:
        while chef.get(x, x) != x:
            chef[x] = chef.get(chef[x], chef[x])
            x = chef[x]
        return x

    for parent, child in links:
        if parent in by_id and child in by_id:
            a, b = boss(parent), boss(child)
            if a != b:
                chef[a] = b

    familien: dict[str, list[dict]] = {}
    for it in items:
        familien.setdefault(boss(it["id"]), []).append(it)
    projekte = []
    for wurzel, mitglieder in familien.items():
        if len(mitglieder) < 2:
            continue
        mitglieder.sort(key=lambda i: i.get("erstellt") or 0)
        key = mitglieder[0]["id"]
        for it in mitglieder:
            it["projekt"] = key
        fertig = sum(1 for i in mitglieder if i["status"] in ERLEDIGT)
        projekte.append({
            "key": key,
            "titel": _projekt_titel([i["titel"] for i in mitglieder]),
            "gesamt": len(mitglieder),
            "fertig": fertig,
            "laeuft": sum(1 for i in mitglieder if i["status"] == "running"),
            "haengt": sum(1 for i in mitglieder if i["status"] == "blocked"),
        })
    # Aktive Projekte zuerst (hängend > laufend > Rest), dann nach Größe.
    projekte.sort(key=lambda p: (-p["haengt"], -p["laeuft"], -p["gesamt"]))
    return projekte


def list_queue() -> dict:
    """Alle nicht archivierten Aufgaben des Boards, jüngste zuerst — die Queue-Sicht der UI."""
    if not _available():
        return {"available": False, "items": [], "offen": 0}

    now = time.time()
    if _list_cache["data"] is not None and now - _list_cache["ts"] < _LIST_EVERY:
        return _list_cache["data"]

    try:
        r = _hermes(["list", "--json", "--sort", "created-desc"])
        raw = _parse_json(r.stdout)
    except Exception:
        log.warning("ideen: kanban list fehlgeschlagen", exc_info=True)
        raw = None
    if not isinstance(raw, list):
        # CLI kaputt/Timeout → ehrlich leer melden, aber nicht cachen (nächster Poll versucht's neu)
        return {"available": True, "items": [], "offen": 0, "fehler": "Queue nicht lesbar"}

    items = []
    for t in raw[:60]:
        try:
            items.append({
                "id": t.get("id", ""),
                "titel": (t.get("title") or "(ohne Titel)")[:200],
                "body": (t.get("body") or "")[:600],
                "status": t.get("status", "?"),
                "assignee": t.get("assignee"),
                "erstellt": t.get("created_at"),
                "fertig": t.get("completed_at"),
                "von": t.get("created_by"),
                "frage": None,      # bei blocked die Worker-Frage (unten nachgeladen)
                "frage_kind": None,
            })
        except Exception:
            continue

    # Hängende Karten anreichern: die Frage des Workers holen, damit die Zentrale sie
    # beantworten kann (Sackgasse-Fix). Deckel gegen Ausreißer, damit ein Poll nie hängt.
    for it in [i for i in items if i["status"] == "blocked"][:6]:
        info = _blocker_frage(it["id"])
        if info:
            it["frage"] = info["frage"]
            it["frage_kind"] = info["kind"]

    projekte = _ketten_anreichern(items)
    offen = sum(1 for i in items if i["status"] not in ("done",))
    data = {"available": True, "items": items, "offen": offen, "projekte": projekte}
    _list_cache.update(ts=now, data=data)
    return data


def add_idea(titel: str, notiz: str = "", created_by: str = "mc2-ui") -> dict:
    """Idee in die Queue legen: triage — der Specifier der Box arbeitet sie selbst aus."""
    if not _available():
        return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
    titel = (titel or "").strip()
    if not (3 <= len(titel) <= 200):
        return {"ok": False, "error": "Titel bitte zwischen 3 und 200 Zeichen."}
    args = ["create", titel, "--triage", "--created-by", created_by, "--json"]
    notiz = (notiz or "").strip()
    if notiz:
        args[2:2] = ["--body", notiz[:4000]]
    try:
        r = _hermes(args)
    except Exception as exc:
        return {"ok": False, "error": f"Queue nicht erreichbar: {exc}"}
    task = _parse_json(r.stdout)
    if r.returncode != 0 or not isinstance(task, dict) or not task.get("id"):
        return {"ok": False, "error": (r.stderr or r.stdout or "kanban create fehlgeschlagen").strip()[:300]}
    _list_cache["ts"] = 0.0  # nächster Poll zeigt die neue Idee sofort
    try:
        from services import announce
        announce.add(f"Neue Idee in der Queue: „{titel}“ ({task['id']}) — die Box arbeitet sie aus.",
                     "[Ideen-Queue]", "ideen-queue", "silent")
    except Exception:
        pass
    return {"ok": True, "id": task["id"], "status": task.get("status")}


def answer_task(task_id: str, antwort: str) -> dict:
    """Eine hängende Aufgabe beantworten: Antwort als Kommentar protokollieren + entsperren.

    `unblock --reason` schreibt die Antwort als „UNBLOCK:"-Kommentar und stellt die Karte auf
    ready — der Dispatcher spawnt den Worker neu, der die Antwort im Task-Kontext vorfindet und
    weitermacht. Genau der Ausweg aus der Sackgasse „Aufgabe fragt, aber niemand kann antworten".
    """
    if not _available():
        return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
    if not _TASK_ID_RX.match(task_id or ""):
        return {"ok": False, "error": f"Keine gültige Aufgaben-Nummer: {task_id!r}"}
    antwort = (antwort or "").strip()
    if not (1 <= len(antwort) <= 2000):
        return {"ok": False, "error": "Bitte eine Antwort zwischen 1 und 2000 Zeichen."}
    try:
        r = _hermes(["unblock", task_id, "--reason", antwort])
    except Exception as exc:
        return {"ok": False, "error": f"Queue nicht erreichbar: {exc}"}
    if r.returncode != 0:
        return {"ok": False, "error": (r.stderr or r.stdout or "Entsperren fehlgeschlagen").strip()[:300]}
    _list_cache["ts"] = 0.0  # nächster Poll zeigt den neuen Status (ready/running) sofort
    try:
        from services import announce
        announce.add(f"Deine Antwort ging an die hängende Aufgabe {task_id} — die Box macht weiter.",
                     "[Ideen-Queue]", "ideen-queue", "silent")
    except Exception:
        pass
    return {"ok": True}


def archive_task(task_id: str) -> dict:
    """Erledigtes/Verworfenes aus der Sicht räumen (Kanban-Archiv, nichts wird gelöscht)."""
    if not _available():
        return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
    if not _TASK_ID_RX.match(task_id or ""):
        return {"ok": False, "error": f"Keine gültige Aufgaben-Nummer: {task_id!r}"}
    try:
        r = _hermes(["archive", task_id])
    except Exception as exc:
        return {"ok": False, "error": f"Queue nicht erreichbar: {exc}"}
    if r.returncode != 0:
        return {"ok": False, "error": (r.stderr or r.stdout or "Archivieren fehlgeschlagen").strip()[:300]}
    _list_cache["ts"] = 0.0
    return {"ok": True}


def ergebnis_of(task_id: str) -> dict:
    """Das ERGEBNIS einer fertigen Aufgabe — die Abschluss-Zusammenfassung des Workers.

    User-Wunsch 15.07.: „im Auftragsbuch werden Dinge getestet, ich sehe aber das
    Ergebnis nicht." Die Wahrheit liegt beim kanban_complete-Aufruf (summary/result)
    im jüngsten `completed`-Event; `list --json` trägt sie nicht → lazy per `show`,
    nur wenn der User die Karte aufklappt. Fertige Ergebnisse ändern sich nie → Cache.
    """
    if not _available():
        return {"available": False, "summary": "", "result": ""}
    if not _TASK_ID_RX.match(task_id or ""):
        return {"available": True, "summary": "", "result": ""}
    if task_id in _erg_cache:
        return _erg_cache[task_id]

    try:
        r = _hermes(["show", task_id, "--json"], timeout=30)
        d = _parse_json(r.stdout)
    except Exception:
        log.warning("ideen: kanban show %s (ergebnis) fehlgeschlagen", task_id, exc_info=True)
        return {"available": True, "summary": "", "result": ""}
    summary, result = "", ""
    if isinstance(d, dict):
        if isinstance(d.get("result"), str):
            result = d["result"].strip()
        for ev in d.get("events") or []:      # chronologisch → letztes completed gewinnt
            if isinstance(ev, dict) and ev.get("kind") == "completed":
                s = ((ev.get("payload") or {}).get("summary") or "").strip()
                if s:
                    summary = s
    data = {"available": True, "summary": summary[:4000], "result": result[:4000]}
    if len(_erg_cache) >= _ERG_MAX:
        _erg_cache.pop(next(iter(_erg_cache)))
    _erg_cache[task_id] = data
    return data


def log_of(task_id: str) -> dict:
    """Worker-Log einer Idee (hermes kanban log <id>), ANSI entfernt, letzte ~120 Zeilen."""
    if not _available():
        return {"available": False, "lines": []}
    if not _TASK_ID_RX.match(task_id or ""):
        return {"available": True, "lines": []}

    now = time.time()
    if (
        _log_cache["data"] is not None
        and _log_cache["task_id"] == task_id
        and now - _log_cache["ts"] < _LOG_EVERY
    ):
        return _log_cache["data"]

    try:
        r = _hermes(["log", task_id])
    except Exception as exc:
        log.warning("ideen: kanban log fehlgeschlagen", exc_info=True)
        return {"available": True, "lines": []}
    if r.returncode != 0:
        log.warning("ideen: kanban log exit=%s stderr=%s", r.returncode, r.stderr)
        return {"available": True, "lines": []}

    raw = r.stdout or ""
    # ANSI-Steuerzeichen entfernen
    ansi = re.compile(r"\x1b\[[0-9;]*m")
    cleaned = ansi.sub("", raw)

    # Letzte ~120 Zeilen
    lines = cleaned.strip().splitlines()[-120:]
    data = {"available": True, "lines": lines}
    _log_cache.update(ts=now, task_id=task_id, data=data)
    return data
