#!/usr/bin/env python3
"""Live HTTP smoke: /council mesh-hosting question — verifier hints + researcher retry + synthesis."""
from __future__ import annotations

import json
import re
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

COUNCIL_FRAME = (
    "Should Mugi run council deliberations on the home Mac only, or allow mesh peers "
    "to host council worker seats when the operator is away?"
)
POLL_S = 20
MAX_WAIT_S = 2700  # 45 min — real MLX seats are slow


def _pgrep_backend_pids() -> list[int]:
    try:
        out = subprocess.check_output(["pgrep", "-f", "mugi_backend"], text=True).strip()
    except subprocess.CalledProcessError:
        return []
    return [int(x) for x in out.splitlines() if x.strip().isdigit()]


def _port_from_pid(pid: int) -> int | None:
    try:
        out = subprocess.check_output(
            ["lsof", "-nP", "-p", str(pid)], text=True, stderr=subprocess.DEVNULL
        )
    except subprocess.CalledProcessError:
        return None
    for line in out.splitlines():
        if "TCP" in line and "LISTEN" in line:
            part = line.split()[-2]
            if ":" in part:
                try:
                    return int(part.rsplit(":", 1)[-1])
                except ValueError:
                    continue
    return None


def _pick_backend() -> tuple[int, int, str]:
    for pid in _pgrep_backend_pids():
        port = _port_from_pid(pid)
        if not port:
            continue
        out = subprocess.check_output(["ps", "eww", "-p", str(pid)], text=True)
        token = ""
        for tok in out.split():
            if tok.startswith("MUGI_AUTH_TOKEN="):
                token = tok.split("=", 1)[1]
                break
        if token:
            return pid, port, token
    raise SystemExit("No listening mugi_backend with MUGI_AUTH_TOKEN found")


def http_json(
    method: str, url: str, headers: dict, body: dict | None = None, timeout: int = 120
) -> tuple[int, object]:
    data = None
    hdrs = dict(headers)
    if body is not None:
        data = json.dumps(body).encode("utf-8")
        hdrs.setdefault("Content-Type", "application/json")
    req = urllib.request.Request(url, data=data, headers=hdrs, method=method)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
            code = resp.getcode()
    except urllib.error.HTTPError as e:
        raw = e.read().decode("utf-8", errors="replace")
        code = e.code
    try:
        parsed = json.loads(raw) if raw.strip() else {}
    except json.JSONDecodeError:
        parsed = raw
    return code, parsed


def _seat_label(task: dict) -> str:
    md = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
    seat = str(md.get("council_seat") or "?")
    attempt = md.get("council_pipeline_attempt") or md.get("pipeline_attempt") or 1
    rnd = md.get("council_round") or 1
    return f"{seat}/a{attempt}/r{rnd}"


def _run_metadata(task_id: str, base: str, h: dict) -> dict:
    code, body = http_json("GET", f"{base}/kanban/tasks/{task_id}/runs", h, timeout=60)
    if code != 200 or not isinstance(body, dict):
        return {}
    runs = body.get("runs") if isinstance(body.get("runs"), list) else []
    for run in runs:
        if not isinstance(run, dict):
            continue
        if (run.get("outcome") or "") != "completed":
            continue
        md = run.get("metadata")
        if isinstance(md, dict):
            return md
    return {}


def main() -> int:
    pid, port, token = _pick_backend()
    base = f"http://127.0.0.1:{port}"
    h = {"X-Auth-Token": token}
    print(f"backend pid={pid} port={port}")

    code, conv = http_json("POST", f"{base}/conversations/new", h)
    if code != 200 or not isinstance(conv, dict) or "thread_id" not in conv:
        print("FAIL: could not create conversation", code, conv, file=sys.stderr)
        return 1
    tid = str(conv["thread_id"])
    print(f"thread_id={tid}")

    slash = f"/council {COUNCIL_FRAME}"
    code, send = http_json(
        "POST", f"{base}/send", h, {"text": slash, "thread_id": tid}, timeout=180
    )
    print(f"POST /send → {code} {send if code != 200 else send.get('command', send)}")
    if code != 200:
        return 1

    time.sleep(3)
    _, boot_msgs = http_json("GET", f"{base}/chat/messages?thread_id={tid}", h, timeout=60)
    boot_list = boot_msgs if isinstance(boot_msgs, list) else boot_msgs.get("messages", [])
    if isinstance(boot_list, list):
        for m in boot_list:
            txt = str(m.get("content") or "")
            if "council limit reached" in txt.lower():
                print(f"FAIL: cap refusal in chat: {txt[:200]}", file=sys.stderr)
                return 3

    seen: dict[str, bool] = {
        "batch_spawned": False,
        "verifier_done": False,
        "verifier_rejections": False,
        "verifier_search_hint": False,
        "researcher_retry": False,
        "synthesis": False,
    }
    batch_id = ""
    verifier_id = ""
    t0 = time.monotonic()
    last_summary = ""

    while time.monotonic() - t0 < MAX_WAIT_S:
        elapsed = int(time.monotonic() - t0)
        q = urllib.parse.urlencode({"thread_id": tid})
        _, state = http_json("GET", f"{base}/chat/thread_state?{q}", h, timeout=30)
        qk = urllib.parse.urlencode({"origin_thread_id": tid, "limit": 30})
        _, kbody = http_json("GET", f"{base}/kanban/tasks?{qk}", h, timeout=60)
        tasks = kbody.get("tasks") if isinstance(kbody, dict) else []
        if not isinstance(tasks, list):
            tasks = []

        council_tasks = [
            t
            for t in tasks
            if isinstance(t, dict)
            and isinstance(t.get("metadata"), dict)
            and t["metadata"].get("council_batch_id")
        ]
        if council_tasks and not batch_id:
            batch_id = str(council_tasks[0]["metadata"].get("council_batch_id") or "")
            seen["batch_spawned"] = True

        lines = []
        for t in sorted(council_tasks, key=lambda x: (x.get("metadata") or {}).get("council_seat", "")):
            lines.append(f"  {_seat_label(t)}: {t.get('status')}")

        for t in council_tasks:
            md = t.get("metadata") if isinstance(t.get("metadata"), dict) else {}
            seat = str(md.get("council_seat") or "")
            attempt = int(md.get("council_pipeline_attempt") or md.get("pipeline_attempt") or 1)
            if seat == "researcher" and attempt >= 2:
                seen["researcher_retry"] = True
            if seat == "verifier" and (t.get("status") or "") == "done":
                seen["verifier_done"] = True
                verifier_id = str(t.get("id") or verifier_id)
                vmd = _run_metadata(verifier_id, base, h) if verifier_id else {}
                rejections = vmd.get("rejections") if isinstance(vmd.get("rejections"), list) else []
                if rejections:
                    seen["verifier_rejections"] = True
                    reasons = " ".join(
                        str(r.get("reason") or "") for r in rejections if isinstance(r, dict)
                    ).lower()
                    hint_markers = (
                        "generic",
                        "broader",
                        "underlying",
                        "category",
                        "mesh",
                        "federat",
                        "peer",
                        "deliberat",
                        "name-collision",
                        "unrelated",
                        "codename",
                        "product",
                    )
                    if any(m in reasons for m in hint_markers):
                        seen["verifier_search_hint"] = True

        _, msgs_body = http_json("GET", f"{base}/chat/messages?thread_id={tid}", h, timeout=60)
        msgs = msgs_body if isinstance(msgs_body, list) else msgs_body.get("messages", [])
        if not isinstance(msgs, list):
            msgs = []
        for m in msgs:
            content = str(m.get("content") or "")
            if m.get("role") == "user" and "Council seats finished" in content:
                seen["synthesis"] = True

        council_active = state.get("council_active") if isinstance(state, dict) else None
        awaiting = state.get("council_awaiting_synthesis") if isinstance(state, dict) else None
        summary = (
            f"[{elapsed:4d}s] active={council_active} awaiting_synth={awaiting} "
            f"tasks={len(council_tasks)} checks={seen}"
        )
        if summary != last_summary:
            print(summary)
            for line in lines:
                print(line)
            last_summary = summary

        if seen["synthesis"]:
            print("\nPASS: synthesis delivered.")
            break

        if (
            seen["batch_spawned"]
            and council_tasks
            and all((t.get("status") or "") in ("done", "cancelled", "gave_up", "blocked") for t in council_tasks)
            and not seen["synthesis"]
            and not (state.get("council_awaiting_synthesis") if isinstance(state, dict) else False)
        ):
            # All seats terminal but no synthesis — keep polling briefly
            pass

        time.sleep(POLL_S)
    else:
        print("\nTIMEOUT waiting for synthesis.", file=sys.stderr)

    print("\n=== Smoke checklist ===")
    for k, v in seen.items():
        print(f"  {'✓' if v else '✗'} {k}")

    required = [
        "batch_spawned",
        "verifier_done",
        "synthesis",
    ]
    optional_retry_path = seen["verifier_rejections"] and (
        seen["researcher_retry"] or seen["verifier_search_hint"]
    )
    if seen["verifier_rejections"] and not seen["researcher_retry"]:
        print(
            "  note: verifier rejected claims but researcher retry not observed "
            "(may have passed on attempt 1 or max attempts reached)"
        )

    ok = all(seen[k] for k in required)
    if seen["verifier_rejections"]:
        print(f"  verifier guidance path: {'✓' if optional_retry_path else 'partial'}")

    return 0 if ok else 2


if __name__ == "__main__":
    raise SystemExit(main())
