"""CLI for the operator bridge — `python -m cmd_chat.operator <verb>`.

Verbs
-----
    serve  HOST PORT USER   run the daemon (normally spawned by `up`, not by hand)
    up     HOST PORT USER   start the daemon detached and wait for it to connect
    read   [--wait]         print new inbox events as JSONL (cursor-tracked)
    say    TEXT…            send a chat line to the room
    roster                  list who's in the room
    status                  daemon + connection state
    down                    stop the daemon, leave the room

Phase 1: join + read + say. The reusable skill wrapper comes in a later phase;
for now a Claude Code session can drive everything straight from Bash.
"""

from __future__ import annotations

import argparse
import json
import sys
import time
from pathlib import Path

from .cli_client import BridgeUnreachable, request
from .session import Session, resolve


def _add_conn_args(p: argparse.ArgumentParser) -> None:
    p.add_argument("host")
    p.add_argument("port", type=int)
    p.add_argument("user", help="room display name to join as (also the session id)")
    p.add_argument("--password", default=None)
    p.add_argument("--no-tls", action="store_true", help="plain ws/http")
    p.add_argument("--insecure", action="store_true", help="skip TLS verify")
    p.add_argument("--session", default=None, help="session id (default: USER)")
    p.add_argument("--trigger", default=None,
                   help="extra phrase that marks a message 'addressed' to us "
                        "(the operator name / @name always count)")


def _build_parser() -> argparse.ArgumentParser:
    ap = argparse.ArgumentParser(prog="cmd_chat.operator")
    sub = ap.add_subparsers(dest="verb", required=True)

    _add_conn_args(sub.add_parser("serve", help="run the daemon (foreground)"))
    _add_conn_args(sub.add_parser("up", help="start the daemon detached"))

    r = sub.add_parser("read", help="print new inbox events")
    r.add_argument("--session", default=None)
    r.add_argument("--since", type=int, default=None,
                   help="seq to read after (default: saved cursor)")
    r.add_argument("--wait", action="store_true", help="long-poll until an event arrives")
    r.add_argument("--timeout", type=float, default=30.0)
    r.add_argument("--all", action="store_true", help="ignore cursor, dump full ring")

    s = sub.add_parser("say", help="send a chat line")
    s.add_argument("text", nargs="+")
    s.add_argument("--session", default=None)

    # ── sandbox drive ───────────────────────────────────────────────────
    sb = sub.add_parser("sbx", help="manage the operator's own container")
    sb.add_argument("action", choices=["launch", "status", "down"], nargs="?",
                    default="status")
    sb.add_argument("--engine", default=None, help="podman|docker (default: auto)")
    sb.add_argument("--image", default=None, help="container image (default per engine)")
    sb.add_argument("--name", default=None, help="container name (default: hh-op-<user>)")
    sb.add_argument("--session", default=None)

    ex = sub.add_parser("exec", help="run a shell command in the sandbox")
    ex.add_argument("cmd", nargs="+")
    ex.add_argument("--session", default=None)

    wr = sub.add_parser("write", help="write a file in the sandbox (content from stdin or --text)")
    wr.add_argument("path")
    wr.add_argument("--text", default=None, help="inline content (else read stdin)")
    wr.add_argument("--session", default=None)

    gt = sub.add_parser("get", help="read a file out of the sandbox")
    gt.add_argument("path")
    gt.add_argument("--out", default=None, help="write to this local path (else stdout)")
    gt.add_argument("--session", default=None)

    ky = sub.add_parser("keys", help="inject keystrokes into the room's shared PTY")
    ky.add_argument("keys", nargs="*",
                    help="tokens: literal text, named keys (enter, ctrl-c, esc, "
                         "up…), text:<verbatim>, hex:<bytes>")
    ky.add_argument("--help-keys", action="store_true", help="print the key vocabulary")
    ky.add_argument("--session", default=None)

    for v in ("roster", "status", "down"):
        sub.add_parser(v).add_argument("--session", default=None)
    return ap


# ── daemon ───────────────────────────────────────────────────────────────
def _run_serve(args) -> int:
    from .bridge import OperatorBridge  # heavy imports only on the daemon path
    bridge = OperatorBridge(
        args.host, args.port, name=args.user, password=args.password,
        insecure=args.insecure, no_tls=args.no_tls,
        session=args.session, trigger=args.trigger)
    try:
        bridge.run()
    except KeyboardInterrupt:
        pass
    return 0


def _run_up(args) -> int:
    import subprocess

    sess = Session(args.session or args.user)
    sess.ensure_dir()

    # Already alive?
    if sess.sock_path.exists():
        try:
            st = request(sess.sock_path, {"op": "status"}, read_timeout=5)
            if st.get("connected"):
                print(f"session '{sess.name}' already up (connected)")
                return 0
        except BridgeUnreachable:
            sess.cleanup()  # stale socket → fall through and respawn

    sess.write_meta(host=args.host, port=args.port, user=args.user,
                    trigger=args.trigger or "", no_tls=args.no_tls,
                    started=time.time())

    cmd = [sys.executable, "-m", "cmd_chat.operator", "serve",
           args.host, str(args.port), args.user]
    if args.password is not None:
        cmd += ["--password", args.password]
    if args.no_tls:
        cmd.append("--no-tls")
    if args.insecure:
        cmd.append("--insecure")
    if args.session:
        cmd += ["--session", args.session]
    if args.trigger:
        cmd += ["--trigger", args.trigger]

    log = open(sess.log_path, "a")  # noqa: SIM115 — handed to the child
    proc = subprocess.Popen(cmd, stdout=log, stderr=log, stdin=subprocess.DEVNULL,
                            start_new_session=True, close_fds=True)

    # Wait for the daemon to connect (or report a fatal-looking error).
    deadline = time.time() + 15.0
    last_err = None
    while time.time() < deadline:
        if proc.poll() is not None:
            print(f"daemon exited early (code {proc.returncode}); see {sess.log_path}",
                  file=sys.stderr)
            return 1
        try:
            st = request(sess.sock_path, {"op": "status"}, read_timeout=3)
        except BridgeUnreachable:
            time.sleep(0.3)
            continue
        if st.get("connected"):
            print(f"session '{sess.name}' up — connected to {args.host}:{args.port} "
                  f"as '{args.user}' (pid {proc.pid})")
            return 0
        # Connected socket but not in-room yet: peek for an error event.
        try:
            rd = request(sess.sock_path, {"op": "read", "since": 0}, read_timeout=3)
            errs = [e for e in rd.get("events", []) if e.get("event") == "error"]
            if errs:
                last_err = errs[-1].get("reason")
        except BridgeUnreachable:
            pass
        time.sleep(0.4)

    msg = f"session '{sess.name}' started (pid {proc.pid}) but not connected yet"
    if last_err:
        msg += f" — last error: {last_err}"
    print(msg + f"; tail {sess.log_path}", file=sys.stderr)
    return 1


# ── client verbs ─────────────────────────────────────────────────────────
def _client_request(args, obj: dict, read_timeout: float = 35.0) -> dict:
    sess = resolve(getattr(args, "session", None))
    if not sess.sock_path.exists():
        print(f"no live bridge for session '{sess.name}' "
              f"(run `up` first)", file=sys.stderr)
        sys.exit(2)
    try:
        return request(sess.sock_path, obj, read_timeout=read_timeout)
    except BridgeUnreachable as e:
        print(str(e), file=sys.stderr)
        sys.exit(2)


def _run_read(args) -> int:
    sess = resolve(getattr(args, "session", None))
    if args.all:
        since = 0
    elif args.since is not None:
        since = args.since
    else:
        since = sess.read_cursor()
    timeout = args.timeout
    resp = _client_request(args, {"op": "read", "since": since,
                                  "wait": args.wait, "timeout": timeout},
                           read_timeout=timeout + 5.0)
    events = resp.get("events", [])
    for ev in events:
        print(json.dumps(ev))
    if events and not args.all and args.since is None:
        sess.write_cursor(events[-1]["seq"])
    return 0


def _run_say(args) -> int:
    text = " ".join(args.text)
    resp = _client_request(args, {"op": "say", "text": text})
    if not resp.get("ok"):
        print(resp.get("error", "say failed"), file=sys.stderr)
        return 1
    return 0


def _run_sbx(args) -> int:
    resp = _client_request(args, {"op": "sbx", "action": args.action,
                                  "engine": args.engine, "image": args.image,
                                  "name": args.name})
    print(json.dumps(resp))
    return 0 if resp.get("ok") else 1


def _run_exec(args) -> int:
    cmd = " ".join(args.cmd)
    resp = _client_request(args, {"op": "exec", "cmd": cmd})
    out = resp.get("output", "")
    if out:
        sys.stdout.write(out if out.endswith("\n") else out + "\n")
    if not resp.get("ok"):
        if "error" in resp:
            print(resp["error"], file=sys.stderr)
        return resp.get("rc", 1) or 1
    return 0


def _run_write(args) -> int:
    text = args.text if args.text is not None else sys.stdin.read()
    resp = _client_request(args, {"op": "write", "path": args.path, "content": text})
    if not resp.get("ok"):
        print(resp.get("error", "write failed"), file=sys.stderr)
        return 1
    print(f"wrote {resp.get('path')} ({resp.get('bytes')} bytes)", file=sys.stderr)
    return 0


def _run_get(args) -> int:
    import base64
    resp = _client_request(args, {"op": "get", "path": args.path})
    if not resp.get("ok"):
        print(resp.get("error", "get failed"), file=sys.stderr)
        return 1
    raw = base64.b64decode(resp.get("b64", ""))
    if args.out:
        Path(args.out).write_bytes(raw)
        print(f"wrote {args.out} ({len(raw)} bytes)", file=sys.stderr)
    else:
        sys.stdout.buffer.write(raw)
    return 0


def _run_keys(args) -> int:
    from . import sandbox as sbx
    if args.help_keys:
        print(sbx.KEYS_HELP)
        return 0
    if not args.keys:
        print("no keys given (try --help-keys)", file=sys.stderr)
        return 2
    resp = _client_request(args, {"op": "keys", "keys": args.keys})
    if not resp.get("ok"):
        print(resp.get("error", "keys failed"), file=sys.stderr)
        return 1
    return 0


def _run_simple(args, op: str) -> int:
    resp = _client_request(args, {"op": op})
    print(json.dumps(resp))
    return 0 if resp.get("ok") else 1


def main(argv: list[str] | None = None) -> int:
    args = _build_parser().parse_args(argv)
    verb = args.verb
    if verb == "serve":
        return _run_serve(args)
    if verb == "up":
        return _run_up(args)
    if verb == "read":
        return _run_read(args)
    if verb == "say":
        return _run_say(args)
    if verb == "sbx":
        return _run_sbx(args)
    if verb == "exec":
        return _run_exec(args)
    if verb == "write":
        return _run_write(args)
    if verb == "get":
        return _run_get(args)
    if verb == "keys":
        return _run_keys(args)
    if verb in ("roster", "status", "down"):
        return _run_simple(args, verb)
    return 2


if __name__ == "__main__":
    sys.exit(main())
