"""bullpen_think — report reasoning excerpts to the thinking display.

Client side of `lmcp-tools/bullpen-think.lua`. Two choices here are deliberate:

* **Not `lmcp-tool think_say text=…`.** An excerpt reaches 64 KiB and grows every
  second; on a command line it would sit in the process list of every user on
  the host, and eventually hit ARG_MAX. So the same route as `bin/room-post`:
  JSON-RPC over HTTP, with the text never touching a shell.
* **Throttled.** A streaming model emits dozens of fragments per second.
  Reporting each one would be a denial-of-service against our own room. The
  interval matches bullseye's own poll cadence (1.5 s) — sending faster than the
  display refreshes is work nobody can see.

The display is explicitly NOT a trust surface: it shows what a worker is
thinking, it proves nothing. The secret still travels along so that no excerpt
can appear under someone else's name.
"""
import json
import os
import time
import urllib.request

_LAST_SENT = {}       # nick -> timestamp of the last accepted report
_INTERVAL_S = float(os.getenv("BULLPEN_THINK_INTERVAL", "1.5"))


def _endpoint():
    host = os.environ.get("LMCP_HOST")
    if not host:
        return None, None
    port = os.environ.get("LMCP_PORT", "8080")
    return f"http://{host}:{port}/mcp", os.environ.get("LMCP_TOKEN", "")


def _secret(nick):
    """This nick's own secret, or "". Never someone else's."""
    for p in (f"/etc/bullpen/post-secret.d/{nick}",
              os.path.expanduser(f"~/.config/bullpen/post-secret.d/{nick}")):
        try:
            s = open(p, encoding="utf-8").read().strip()
            if s:
                return s
        except OSError:
            pass
    return ""


def _tool_result(raw):
    """Unwrap a tools/call response down to the tool's own JSON.

    Checking only the HTTP status is not enough, and this file used to make
    exactly that mistake: lmcp answers 200 with `{"ok":false,"error":…}` in the
    body when a tool refuses. A caller that trusts the status code reports
    success for a rejected write — the same worthless `ok:true` that cost this
    project a full day on 2026-08-08. Parse it or don't claim anything.

    Returns the decoded object, or None if the shape is not what we expect.
    """
    for line in raw.splitlines():             # SSE framing, when present
        if line.startswith("data: "):
            raw = line[6:]
            break
    try:
        env = json.loads(raw)
    except Exception:
        return None
    if isinstance(env.get("error"), dict):
        return {"ok": False, "error": env["error"].get("message", "rpc_error")}
    content = ((env.get("result") or {}).get("content") or [])
    if not content:
        return None
    text = content[0].get("text")
    if not isinstance(text, str):
        return None
    try:
        return json.loads(text)
    except Exception:
        return None


def report(nick, text, run_id=None, force=False):
    """Report a reasoning excerpt. True only when the room ACCEPTED it.

    `force=True` bypasses the throttle — use it for the final state at the end
    of a run, so the display does not freeze on an arbitrary intermediate.

    Never raises: a broken display must never kill a worker. The display is
    decoration, the worker is the work. But it does not lie either — a refused
    write returns False.
    """
    now = time.time()
    if not force and (now - _LAST_SENT.get(nick, 0)) < _INTERVAL_S:
        return False
    url, token = _endpoint()
    if not url:
        return False

    args = {"from": nick, "text": text or ""}
    if run_id is not None:
        args["id"] = str(run_id)
    s = _secret(nick)
    if s:
        args["secret"] = s

    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
                       "params": {"name": "think_say", "arguments": args}}).encode()
    req = urllib.request.Request(url, data=body, headers={
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
        "Authorization": f"Bearer {token}"})
    try:
        raw = urllib.request.urlopen(req, timeout=8).read().decode()
    except Exception:
        return False

    result = _tool_result(raw)
    if not isinstance(result, dict) or result.get("ok") is not True:
        return False
    # Only now does the throttle advance: a refused write must not buy silence
    # until the next interval.
    _LAST_SENT[nick] = now
    return True
