"""MC2-Zusatzmodul (20.07.2026) — Orphan-Reaper für Kanban-Worker.

Wird vom Patch-Träger (``deploy/hermes-patches/apply.py``) nach
``~/.hermes/hermes-agent/hermes_cli/`` kopiert und vom Gateway-Dispatcher
(``gateway/kanban_watchers.py``, per Patch 0003) jeden Tick aufgerufen.

Problem: Wird eine ``running``-Karte von außen blockiert/zurückgesetzt (z. B.
über das Auftragsbuch oder die No-Progress-Bremse), setzt ``block_task``
``worker_pid = NULL`` — der Worker-PROZESS läuft aber weiter. Die DB zählt ihn
nicht mehr, also unterläuft der Orphan ``kanban.max_in_progress`` (Doppel-Worker
möglich) und verbrennt Rechenzeit. Dieser Reaper beendet lebende Worker, deren
Task nicht mehr ``running`` ist.

Linux-only (/proc-Scan). Best-effort: jeder Fehler → leere Liste, nie werfen.
"""
from __future__ import annotations

import os
import re
import signal
import sys

_TASK_RE = re.compile(rb"work kanban task (t_[0-9a-fA-F]+)")


def reap_orphaned_workers(connect_closing) -> "list[tuple[int, str]]":
    """Beende lebende Kanban-Worker, deren Task nicht mehr ``running`` ist.

    ``connect_closing`` ist die gleichnamige Kontextmanager-Factory aus
    ``kanban_db`` (durchgereicht, um Import-Zyklen zu vermeiden). Gibt die Liste
    der beendeten ``(pid, task_id)`` zurück.
    """
    if sys.platform != "linux":
        return []
    candidates: "dict[int, str]" = {}
    try:
        entries = os.listdir("/proc")
    except OSError:
        return []
    for entry in entries:
        if not entry.isdigit():
            continue
        try:
            with open(f"/proc/{entry}/cmdline", "rb") as fh:
                cmd = fh.read().replace(b"\x00", b" ")
        except OSError:
            continue
        m = _TASK_RE.search(cmd)
        if m:
            candidates[int(entry)] = m.group(1).decode()
    if not candidates:
        return []

    task_ids = list(set(candidates.values()))
    placeholders = ",".join("?" * len(task_ids))
    running: "set[str]" = set()
    try:
        with connect_closing() as conn:
            running = {
                row[0]
                for row in conn.execute(
                    f"SELECT id FROM tasks WHERE id IN ({placeholders}) "
                    "AND status = 'running'",
                    task_ids,
                )
            }
    except Exception:
        return []

    killed: "list[tuple[int, str]]" = []
    for pid, task_id in candidates.items():
        if task_id in running:
            continue
        # Prozessgruppe zuerst (Worker starten start_new_session=True → eigene
        # Gruppe; killpg räumt selbst gestartete Kinder wie uvicorn gleich mit).
        try:
            os.killpg(os.getpgid(pid), signal.SIGTERM)
        except OSError:
            try:
                os.kill(pid, signal.SIGTERM)
            except OSError:
                continue
        killed.append((pid, task_id))
    return killed
