"""
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 subprocess
import time
from pathlib import Path

log = logging.getLogger(__name__)

HERMES = Path(os.environ.get("MC_HERMES_BIN", "~/.local/bin/hermes")).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


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 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"]

    offen = sum(1 for i in items if i["status"] not in ("done",))
    data = {"available": True, "items": items, "offen": offen}
    _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 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
