#!/usr/bin/env python3
"""Ask the bullpen and block until the reply — the rich-agent "visit, ask, leave".
  room-ask [--from NICK] [--timeout N] [--liveness|--no-liveness] [--liveness-cmd CMD] [--liveness-interval N] @worker <question...>
Posts an `ask` addressed to @worker, waits for the `reply` that answers it, prints
the reply body. Exit 0 on reply, 1 on timeout/error. The wait is client-side (this
process), so it never blocks the lmcp server.

The liveness watch on the worker's servant is ON by default (flip per contract
C-2b-T2x-flip, soak: Kampagne 3, 30 asks, 0 false aborts): if the worker is judged
dead (2 consecutive failed probes), abort early with exit 3 instead of waiting for
the full timeout. Opt out with --no-liveness or ROOM_ASK_LIVENESS=0; flags beat env.
"""
import sys, json, subprocess, time, os, socket, shlex

# R4: bullpen.lua weist einen Post eines PRIVILEGIERTEN Nicks (markus, noether,
# foreman, ...) ohne dieses Secret mit "unauthorized" zurueck. Fuer einen nicht
# privilegierten Nick ignoriert das Gatter den Wert - mitschicken ist also immer
# richtig und nie schaedlich. Zwei Pfade, wie in bullpen_worker.py: der /etc-Pfad
# gehoert root (die Daemons laufen so), der Nutzerpfad ist fuer CLI-Aufrufer wie
# diesen hier gedacht.
def _post_secret():
    for p in ("/etc/bullpen/post-secret",
              os.path.expanduser("~/.config/bullpen/post-secret")):
        try:
            s = open(p).read().strip()
            if s:
                return s
        except OSError:
            pass
    return ""

POST_SECRET = _post_secret()

def lmcp(tool, **kw):
    return subprocess.run(["lmcp-tool", tool] + [f"{k}={v}" for k, v in kw.items()],
                          capture_output=True, text=True)

def probe_worker(cmd, nick, timeout_sec=20):
    """Run the liveness probe command.

    Returns tuple: (verdict, stderr_line) where verdict is "alive", "dead", or "unknown".
    Stderr line is passed through for diagnostics (one line, prefix kept).
    """
    try:
        # cmd is a string (possibly multiple words), append nick as last arg
        argv = shlex.split(cmd) + [nick]
        r = subprocess.run(argv, capture_output=True, text=True, timeout=timeout_sec)
        stderr_line = (r.stderr or "").strip().split('\n')[0] if r.stderr else ""
        if r.returncode == 0:
            return ("alive", stderr_line)
        elif r.returncode == 1:
            return ("dead", stderr_line)
        else:
            return ("unknown", stderr_line)
    except Exception as e:
        # Timeout, exec failure, etc. are all UNKNOWN (fail-safe)
        return ("unknown", str(e))

def main():
    args = sys.argv[1:]
    frm = os.environ.get("ROOM_NICK") or socket.gethostname().split(".")[0]
    # 90 statt 60: der Poll-Takt der Worker liegt seit 2026-08-03 bei 10 s
    # (0,1 Hz), ein gemessener Rundlauf mit @dispatcher dauerte 54 s.
    timeout = int(os.environ.get("ROOM_ASK_TIMEOUT", "90"))

    # Liveness probe configuration. Default-on since 0.2.10 (C-2b-T2x-flip;
    # O5 criterion met: one full opt-in campaign, 30 asks, 0 false aborts,
    # 5 alive-but-silent waits ran to expiry). liveness_flag tracks an
    # EXPLICIT flag decision; flags beat env per contract paragraph 2.1.
    liveness_flag = None   # True (--liveness), False (--no-liveness), None
    liveness_cmd = None
    liveness_interval = None  # will be determined after parsing

    pos = []
    i = 0
    while i < len(args):
        if args[i] == "--from" and i + 1 < len(args): frm = args[i + 1]; i += 2
        elif args[i] == "--timeout" and i + 1 < len(args): timeout = int(args[i + 1]); i += 2
        elif args[i] == "--liveness": liveness_flag = True; i += 1
        elif args[i] == "--no-liveness": liveness_flag = False; i += 1
        elif args[i] == "--liveness-cmd" and i + 1 < len(args):
            # A custom cmd implies the watch, but never overrides an
            # explicit --liveness/--no-liveness (explicit beats implicit).
            liveness_cmd = args[i + 1]
            if liveness_flag is None:
                liveness_flag = True
            i += 2
        elif args[i] == "--liveness-interval" and i + 1 < len(args):
            try:
                liveness_interval = int(args[i + 1])
            except ValueError:
                print("usage: room-ask [--from NICK] [--timeout N] [--liveness|--no-liveness] [--liveness-cmd CMD] [--liveness-interval N] @worker <question>", file=sys.stderr)
                sys.exit(2)
            # Validate immediately (contract §2.1: precedes posting)
            if liveness_interval <= 0:
                print("usage: room-ask [--from NICK] [--timeout N] [--liveness|--no-liveness] [--liveness-cmd CMD] [--liveness-interval N] @worker <question>", file=sys.stderr)
                sys.exit(2)
            i += 2
        else: pos.append(args[i]); i += 1

    # Resolve the watch: flags beat env beats default-on (contract 2.1 /
    # C-2b-T2x-flip). Any other env value than "0"/"1" counts as unset.
    if liveness_flag is not None:
        liveness_enabled = liveness_flag
    elif os.environ.get("ROOM_ASK_LIVENESS") in ("0", "1"):
        liveness_enabled = os.environ["ROOM_ASK_LIVENESS"] == "1"
    else:
        liveness_enabled = True
    if liveness_cmd is None:
        liveness_cmd = os.environ.get("ROOM_ASK_LIVENESS_CMD")
    if liveness_interval is None and "ROOM_ASK_LIVENESS_INTERVAL" in os.environ:
        # R1 (review 2026-08-18), restated for default-on (review F2,
        # 2026-08-21): a stray env var in a daemon environment must stay
        # inert unless the operator EXPLICITLY opted in. Pre-flip "watch
        # enabled" implied explicit opt-in; post-flip it does not, so the
        # loud exit 2 is now gated on the explicit enables (flag, or env
        # ROOM_ASK_LIVENESS=1). On the default path an invalid value is
        # warned about and replaced by the 90s default -- never fatal.
        # Flags above are still validated unconditionally (precede posting).
        raw = os.environ["ROOM_ASK_LIVENESS_INTERVAL"]
        try:
            v = int(raw)
        except ValueError:
            v = None
        if v is not None and v > 0:
            liveness_interval = v
        elif liveness_enabled:
            if liveness_flag is True or os.environ.get("ROOM_ASK_LIVENESS") == "1":
                print("usage: room-ask [--from NICK] [--timeout N] [--liveness|--no-liveness] [--liveness-cmd CMD] [--liveness-interval N] @worker <question>", file=sys.stderr)
                sys.exit(2)
            print(f"room-ask: ignoring invalid ROOM_ASK_LIVENESS_INTERVAL={raw!r}, using default 90s",
                  file=sys.stderr)

    # Set default interval if not specified (contract O3: 90s)
    if liveness_interval is None:
        liveness_interval = 90

    # Set default probe command if liveness enabled and no cmd specified
    if liveness_enabled and liveness_cmd is None:
        # Try to use neighbor bullpen-doctor if it exists, else PATH-resolved
        doctor_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "bin", "bullpen-doctor")
        if os.path.exists(doctor_path):
            liveness_cmd = f"{doctor_path} --probe"
        else:
            liveness_cmd = "bullpen-doctor --probe"

    if len(pos) < 2:
        print("usage: room-ask [--from NICK] [--timeout N] [--liveness|--no-liveness] [--liveness-cmd CMD] [--liveness-interval N] @worker <question>", file=sys.stderr)
        sys.exit(2)
    to = pos[0] if pos[0].startswith("@") else "@" + pos[0]
    body = " ".join(pos[1:])

    # Extract nick without @ for probe
    nick_for_probe = to.lstrip("@")

    kw = {"from": frm, "to": to, "type": "ask", "body": body}
    if POST_SECRET:
        kw["secret"] = POST_SECRET
    r = lmcp("room_say", **kw)
    try:
        ask_id = json.loads(r.stdout.strip())["id"]
    except Exception:
        hinweis = ""
        if "unauthorized" in (r.stdout or "") and not POST_SECRET:
            hinweis = (f"\n  '{frm}' ist ein privilegierter Nick und braucht das Post-Secret."
                       "\n  Weder /etc/bullpen/post-secret noch ~/.config/bullpen/post-secret"
                       " war lesbar.")
        print(f"room-ask: post failed: {r.stdout}{r.stderr}{hinweis}", file=sys.stderr); sys.exit(1)

    deadline = time.time() + timeout
    t_ask = time.time()
    dead_count = 0
    next_probe_time = t_ask + liveness_interval if liveness_enabled else None

    while time.time() < deadline:
        now = time.time()

        # Run probe if it's time and liveness is enabled
        if liveness_enabled and next_probe_time is not None and now >= next_probe_time:
            probe_result, probe_stderr = probe_worker(liveness_cmd, nick_for_probe)
            if probe_result == "alive":
                dead_count = 0
            elif probe_result == "dead":
                dead_count += 1
            # "unknown" leaves dead_count unchanged
            # Pass through probe stderr diagnostics
            if probe_stderr:
                sys.stderr.write(f"probe: {probe_stderr}\n")

            next_probe_time = now + liveness_interval

            # Check abort condition: 2 consecutive dead probes
            if dead_count >= 2:
                # Mandatory final poll before abort
                for line in lmcp("room_read", since=ask_id).stdout.splitlines():
                    line = line.strip()
                    if not line: continue
                    try: m = json.loads(line)
                    except Exception: continue
                    if m.get("type") == "reply" and m.get("in_reply_to") == ask_id:
                        print(m.get("body", "")); sys.exit(0)

                # No reply found; abort
                elapsed = int(now - t_ask)
                print(f"room-ask: abort: worker {to} dead (2 consecutive dead probes, interval {liveness_interval}s, t={elapsed}s)", file=sys.stderr)
                sys.exit(3)

        # Reply polling (unchanged)
        for line in lmcp("room_read", since=ask_id).stdout.splitlines():
            line = line.strip()
            if not line: continue
            try: m = json.loads(line)
            except Exception: continue
            if m.get("type") == "reply" and m.get("in_reply_to") == ask_id:
                print(m.get("body", "")); sys.exit(0)

        time.sleep(1.5)

    print(f"room-ask: no reply from {to} within {timeout}s", file=sys.stderr); sys.exit(1)

if __name__ == "__main__":
    main()
