import argparse
import json
import os
import queue
import shlex
import socket
import subprocess
import sys
import threading
import time
import uuid
import webbrowser
from contextlib import contextmanager
from importlib import metadata as importlib_metadata
from pathlib import Path
from urllib.parse import quote_plus
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

from hiveos.intelligence.tracing import build_trace_event, emit_trace, read_trace_events
from hiveos.intelligence import tracing as tracing_module


def _now_ms():
    return int(time.time() * 1000)


def _shorten(value, limit=800):
    text = str(value or "")
    if len(text) <= limit:
        return text
    return text[: max(0, limit - 3)] + "..."


def _build_run_id(prefix="observe"):
    return f"{prefix}:{uuid.uuid4().hex[:12]}"


def _hive_version():
    for name in ("hiveos-trace", "hiveos"):
        try:
            return str(importlib_metadata.version(name))
        except importlib_metadata.PackageNotFoundError:
            continue
    return "dev"


def _active_trace_log_path():
    # Resolve to an absolute path so `trace show` works even when cwd changes.
    return str(Path(str(tracing_module.TRACE_LOG_PATH or "trace_events.log")).expanduser().resolve())


def _trace_home():
    configured = str(os.environ.get("HIVE_TRACE_HOME") or "").strip()
    if configured:
        return Path(configured)
    return Path.home() / ".hiveos-trace"


def _runs_dir():
    path = _trace_home() / "runs"
    path.mkdir(parents=True, exist_ok=True)
    return path


def _run_file(run_id):
    safe = str(run_id or "").replace(":", "_")
    return _runs_dir() / f"{safe}.json"


def _persist_run_record(record):
    path = _run_file(record.get("run_id"))
    path.write_text(json.dumps(record, indent=2), encoding="utf-8")
    return path


def _read_run_record(run_id):
    path = _run_file(run_id)
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return None


def _list_run_records(limit=100):
    records = []
    for path in _runs_dir().glob("*.json"):
        try:
            record = json.loads(path.read_text(encoding="utf-8"))
            if isinstance(record, dict):
                records.append(record)
        except Exception:
            continue
    records.sort(key=lambda item: int(item.get("started_at_ms") or 0), reverse=True)
    return records[: max(1, min(1000, int(limit or 100)))]


def _iter_trace_files_from_base_oldest_first(base_path):
    text = str(base_path or "").strip()
    if not text:
        return []
    path = Path(text)
    parent = path.parent
    name = path.name
    rotated = []
    try:
        for candidate in parent.glob(f"{name}.*"):
            suffix = candidate.name[len(name) + 1 :]
            if suffix.isdigit():
                rotated.append((int(suffix), candidate))
    except Exception:
        rotated = []
    rotated.sort(key=lambda item: item[0], reverse=True)
    files = [candidate for _, candidate in rotated]
    files.append(path)
    return [item for item in files if item.exists()]


def _read_trace_events_for_run(run_id, *, limit=200, trace_log_path=""):
    from collections import deque

    rid = str(run_id or "").strip()
    if not rid:
        return []
    out = deque(maxlen=max(1, int(limit or 200)))
    candidates = []
    preferred = str(trace_log_path or "").strip()
    if preferred:
        candidates.extend(_iter_trace_files_from_base_oldest_first(preferred))
    active = _active_trace_log_path()
    if active and active != preferred:
        candidates.extend(_iter_trace_files_from_base_oldest_first(active))
    seen_paths = set()
    for path in candidates:
        norm = str(path)
        if norm in seen_paths:
            continue
        seen_paths.add(norm)
        try:
            with path.open("r", encoding="utf-8") as handle:
                for line in handle:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        event = json.loads(line)
                    except Exception:
                        continue
                    if str(event.get("run_id") or "") == rid:
                        out.append(event)
        except OSError:
            continue
    return list(out)


def emit_observe_event(
    *,
    run_id,
    event_type,
    outcome="success",
    payload=None,
    agent_id="observer.sdk",
    task_id=None,
    chunk_id=None,
    zone_id=None,
):
    event = build_trace_event(
        event_type=event_type,
        run_id=run_id,
        outcome=outcome if outcome in {"success", "failed", "denied"} else "success",
        payload=payload or {},
        agent_id=agent_id,
        task_id=task_id,
        chunk_id=chunk_id,
        zone_id=zone_id,
    )
    return emit_trace(event)


@contextmanager
def observe_run(
    run_name,
    *,
    run_id=None,
    metadata=None,
    agent_id="observer.sdk",
):
    rid = str(run_id or _build_run_id())
    started_at = _now_ms()
    emit_observe_event(
        run_id=rid,
        event_type="observe_run_started",
        outcome="success",
        payload={
            "run_name": str(run_name or "run"),
            "metadata": dict(metadata or {}),
            "source": "sdk",
        },
        agent_id=agent_id,
    )
    try:
        yield rid
        elapsed_ms = max(0, _now_ms() - started_at)
        emit_observe_event(
            run_id=rid,
            event_type="observe_run_finished",
            outcome="success",
            payload={"run_name": str(run_name or "run"), "duration_ms": elapsed_ms, "source": "sdk"},
            agent_id=agent_id,
        )
    except Exception as exc:
        elapsed_ms = max(0, _now_ms() - started_at)
        emit_observe_event(
            run_id=rid,
            event_type="observe_run_finished",
            outcome="failed",
            payload={
                "run_name": str(run_name or "run"),
                "duration_ms": elapsed_ms,
                "error": _shorten(str(exc), limit=400),
                "source": "sdk",
            },
            agent_id=agent_id,
        )
        raise


def observe_step(run_id, step_name, *, payload=None, outcome="success", agent_id="observer.sdk"):
    return emit_observe_event(
        run_id=run_id,
        event_type="observe_step",
        outcome=outcome,
        payload={"step_name": str(step_name or "step"), "source": "sdk", **dict(payload or {})},
        agent_id=agent_id,
    )


def observe_checkpoint(
    run_id,
    checkpoint_id,
    *,
    step_name="",
    state_ref="",
    payload=None,
    agent_id="observer.sdk",
):
    body = {
        "checkpoint_id": str(checkpoint_id or "").strip(),
        "step_name": str(step_name or "").strip(),
        "state_ref": str(state_ref or "").strip(),
        "source": "sdk",
    }
    body.update(dict(payload or {}))
    if not body["checkpoint_id"]:
        raise ValueError("checkpoint_id is required")
    return emit_observe_event(
        run_id=run_id,
        event_type="observe_checkpoint",
        outcome="success",
        payload=body,
        agent_id=agent_id,
    )


def observe_rerun_request(
    run_id,
    *,
    from_checkpoint_id,
    reason="",
    overrides=None,
    agent_id="observer.sdk",
):
    checkpoint_id = str(from_checkpoint_id or "").strip()
    if not checkpoint_id:
        raise ValueError("from_checkpoint_id is required")
    return emit_observe_event(
        run_id=run_id,
        event_type="observe_rerun_requested",
        outcome="success",
        payload={
            "from_checkpoint_id": checkpoint_id,
            "reason": str(reason or "").strip(),
            "overrides": dict(overrides or {}),
            "source": "sdk",
        },
        agent_id=agent_id,
    )


def _reader_thread(stream, label, out_queue):
    for line in iter(stream.readline, ""):
        out_queue.put((label, line))
    out_queue.put((label, None))


def _stream_process_output(proc, run_id, command_text, agent_id):
    out_queue = queue.Queue()
    stdout_thread = threading.Thread(target=_reader_thread, args=(proc.stdout, "stdout", out_queue), daemon=True)
    stderr_thread = threading.Thread(target=_reader_thread, args=(proc.stderr, "stderr", out_queue), daemon=True)
    stdout_thread.start()
    stderr_thread.start()
    closed = set()
    seq = 0
    while len(closed) < 2:
        channel, chunk = out_queue.get()
        if chunk is None:
            closed.add(channel)
            continue
        seq += 1
        if channel == "stdout":
            sys.stdout.write(chunk)
            sys.stdout.flush()
        else:
            sys.stderr.write(chunk)
            sys.stderr.flush()
        emit_observe_event(
            run_id=run_id,
            event_type="observe_output",
            outcome="success",
            payload={
                "step_name": "command.output",
                "command": command_text,
                "stream": channel,
                "seq": seq,
                "text": _shorten(chunk.rstrip("\n"), limit=2000),
                "source": "cli",
            },
            agent_id=agent_id,
        )
    stdout_thread.join(timeout=0.1)
    stderr_thread.join(timeout=0.1)


class _ObserveProxyServer:
    def __init__(self, *, run_id, upstream_base, bind_host="127.0.0.1", bind_port=0, agent_id="observer.proxy"):
        self.run_id = str(run_id or "")
        self.upstream_base = str(upstream_base or "").rstrip("/")
        self.bind_host = str(bind_host or "127.0.0.1")
        self.bind_port = int(bind_port or 0)
        self.agent_id = str(agent_id or "observer.proxy")
        self._server = None
        self._thread = None
        self.url = ""

    def start(self):
        server_ref = self

        class _Handler(BaseHTTPRequestHandler):
            def do_POST(self):
                server_ref._handle_request(self)

            def do_GET(self):
                server_ref._handle_request(self)

            def log_message(self, fmt, *args):  # noqa: A003
                return

        self._server = ThreadingHTTPServer((self.bind_host, self.bind_port), _Handler)
        actual_host, actual_port = self._server.server_address
        self.url = f"http://{actual_host}:{actual_port}/v1"
        self._thread = threading.Thread(target=self._server.serve_forever, daemon=True, name="hive-observe-proxy")
        self._thread.start()
        emit_observe_event(
            run_id=self.run_id,
            event_type="observe_proxy_started",
            outcome="success",
            payload={"proxy_url": self.url, "upstream_base": self.upstream_base, "source": "cli"},
            agent_id=self.agent_id,
        )
        return self

    def stop(self):
        if self._server:
            try:
                self._server.shutdown()
            except Exception:
                pass
            try:
                self._server.server_close()
            except Exception:
                pass
            self._server = None
        if self._thread:
            self._thread.join(timeout=1.0)
            self._thread = None
        if self.url:
            emit_observe_event(
                run_id=self.run_id,
                event_type="observe_proxy_stopped",
                outcome="success",
                payload={"proxy_url": self.url, "source": "cli"},
                agent_id=self.agent_id,
            )

    def _target_url(self, path):
        p = str(path or "/")
        if not p.startswith("/"):
            p = "/" + p
        if self.upstream_base.endswith("/v1") and p.startswith("/v1/"):
            p = p[3:]
        if self.upstream_base.endswith("/v1") and p == "/v1":
            p = ""
        return f"{self.upstream_base}{p}"

    def _handle_request(self, handler):
        started = _now_ms()
        content_length = int(handler.headers.get("Content-Length") or 0)
        body = handler.rfile.read(content_length) if content_length > 0 else b""
        path = str(handler.path or "/")

        emit_observe_event(
            run_id=self.run_id,
            event_type="observe_proxy_request",
            outcome="success",
            payload={
                "method": str(handler.command or "GET"),
                "path": path,
                "body_bytes": len(body),
                "source": "proxy",
            },
            agent_id=self.agent_id,
        )

        url = self._target_url(path)
        forward_headers = {}
        for key, value in handler.headers.items():
            k = str(key or "")
            if k.lower() in {"host", "content-length"}:
                continue
            forward_headers[k] = value
        req = urllib.request.Request(url=url, data=(body if body else None), headers=forward_headers, method=handler.command)
        try:
            with urllib.request.urlopen(req, timeout=120) as resp:
                resp_body = resp.read()
                status_code = int(resp.status)
                resp_headers = dict(resp.headers.items())
            elapsed = max(0, _now_ms() - started)
            emit_observe_event(
                run_id=self.run_id,
                event_type="observe_proxy_response",
                outcome="success",
                payload={
                    "method": str(handler.command or "GET"),
                    "path": path,
                    "status_code": status_code,
                    "duration_ms": elapsed,
                    "body_tail": _shorten(resp_body.decode("utf-8", errors="replace"), limit=2000),
                    "source": "proxy",
                },
                agent_id=self.agent_id,
            )
            handler.send_response(status_code)
            for header_key, header_value in resp_headers.items():
                hk = str(header_key or "")
                if hk.lower() in {"transfer-encoding", "content-length", "connection"}:
                    continue
                handler.send_header(hk, header_value)
            handler.send_header("Content-Length", str(len(resp_body)))
            handler.end_headers()
            if resp_body:
                handler.wfile.write(resp_body)
        except urllib.error.HTTPError as exc:
            err_body = exc.read() if hasattr(exc, "read") else b""
            status_code = int(getattr(exc, "code", 502) or 502)
            elapsed = max(0, _now_ms() - started)
            emit_observe_event(
                run_id=self.run_id,
                event_type="observe_proxy_response",
                outcome="failed",
                payload={
                    "method": str(handler.command or "GET"),
                    "path": path,
                    "status_code": status_code,
                    "duration_ms": elapsed,
                    "error": _shorten(str(exc), limit=400),
                    "body_tail": _shorten(err_body.decode("utf-8", errors="replace"), limit=2000),
                    "source": "proxy",
                },
                agent_id=self.agent_id,
            )
            handler.send_response(status_code)
            handler.send_header("Content-Type", "application/json")
            handler.send_header("Content-Length", str(len(err_body)))
            handler.end_headers()
            if err_body:
                handler.wfile.write(err_body)
        except Exception as exc:
            elapsed = max(0, _now_ms() - started)
            payload = json.dumps({"error": "proxy_forward_failed", "detail": str(exc)}, separators=(",", ":")).encode("utf-8")
            emit_observe_event(
                run_id=self.run_id,
                event_type="observe_proxy_response",
                outcome="failed",
                payload={
                    "method": str(handler.command or "GET"),
                    "path": path,
                    "status_code": 502,
                    "duration_ms": elapsed,
                    "error": _shorten(str(exc), limit=400),
                    "source": "proxy",
                },
                agent_id=self.agent_id,
            )
            handler.send_response(502)
            handler.send_header("Content-Type", "application/json")
            handler.send_header("Content-Length", str(len(payload)))
            handler.end_headers()
            handler.wfile.write(payload)


@contextmanager
def observe_proxy(*, run_id, upstream_base, bind_host="127.0.0.1", bind_port=0, agent_id="observer.proxy"):
    server = _ObserveProxyServer(
        run_id=run_id,
        upstream_base=upstream_base,
        bind_host=bind_host,
        bind_port=bind_port,
        agent_id=agent_id,
    ).start()
    try:
        yield server
    finally:
        server.stop()


def _build_proxy_env(proxy_url):
    url = str(proxy_url or "").strip().rstrip("/")
    return {
        "OPENAI_BASE_URL": url,
        "OPENAI_API_BASE": url,
    }


def run_command_observed(
    command,
    *,
    run_id=None,
    run_name=None,
    cwd=None,
    timeout_sec=None,
    agent_id="observer.cli",
    open_ui_url="",
    env_overrides=None,
):
    if isinstance(command, str):
        command_text = command.strip()
        if not command_text:
            raise ValueError("command must be non-empty")
        argv = shlex.split(command_text, posix=(os.name != "nt"))
    else:
        argv = [str(part) for part in (command or []) if str(part)]
        command_text = " ".join(argv).strip()
    if not argv:
        raise ValueError("command must be non-empty")

    rid = str(run_id or _build_run_id("observe-run"))
    started_at = _now_ms()
    record = {
        "run_id": rid,
        "run_name": str(run_name or command_text),
        "command": command_text,
        "cwd": str(cwd or ""),
        "trace_log_path": _active_trace_log_path(),
        "started_at_ms": started_at,
        "status": "running",
        "exit_code": None,
        "duration_ms": None,
        "source": "cli",
    }
    _persist_run_record(record)
    emit_observe_event(
        run_id=rid,
        event_type="observe_run_started",
        outcome="success",
        payload={
            "run_name": record["run_name"],
            "command": command_text,
            "cwd": record["cwd"],
            "source": "cli",
        },
        agent_id=agent_id,
    )
    emit_observe_event(
        run_id=rid,
        event_type="observe_step",
        outcome="success",
        payload={"step_name": "command.start", "command": command_text, "source": "cli"},
        agent_id=agent_id,
    )

    proc = subprocess.Popen(
        argv,
        cwd=cwd or None,
        env={**os.environ, **dict(env_overrides or {})},
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        bufsize=1,
        universal_newlines=True,
        shell=False,
    )
    timed_out = False
    try:
        _stream_process_output(proc, rid, command_text, agent_id)
        proc.wait(timeout=timeout_sec if timeout_sec and timeout_sec > 0 else None)
    except subprocess.TimeoutExpired:
        timed_out = True
        proc.kill()
        proc.wait(timeout=2)
    finally:
        try:
            if proc.stdout:
                proc.stdout.close()
        except Exception:
            pass
        try:
            if proc.stderr:
                proc.stderr.close()
        except Exception:
            pass

    elapsed_ms = max(0, _now_ms() - started_at)
    exit_code = int(proc.returncode if proc.returncode is not None else 1)
    if timed_out:
        exit_code = 124
    ok = (not timed_out) and exit_code == 0
    outcome = "success" if ok else "failed"

    emit_observe_event(
        run_id=rid,
        event_type="observe_step",
        outcome=outcome,
        payload={
            "step_name": "command.finish",
            "duration_ms": elapsed_ms,
            "exit_code": exit_code,
            "timeout_sec": timeout_sec if timed_out else None,
            "source": "cli",
        },
        agent_id=agent_id,
    )
    emit_observe_event(
        run_id=rid,
        event_type="observe_run_finished",
        outcome=outcome,
        payload={
            "run_name": record["run_name"],
            "command": command_text,
            "duration_ms": elapsed_ms,
            "exit_code": exit_code,
            "error": "timeout_expired" if timed_out else "",
            "source": "cli",
        },
        agent_id=agent_id,
    )
    record["status"] = "success" if ok else "failed"
    record["exit_code"] = exit_code
    record["duration_ms"] = elapsed_ms
    record["finished_at_ms"] = _now_ms()
    _persist_run_record(record)

    if open_ui_url:
        _open_run_in_ui(open_ui_url, rid)
    return {
        "run_id": rid,
        "exit_code": exit_code,
        "duration_ms": elapsed_ms,
        "status": record["status"],
    }


def _build_parser():
    parser = argparse.ArgumentParser(
        prog="hive",
        description="HiveOS Trace harness: wrap workflows, inspect routes, and open runs.",
    )
    parser.add_argument(
        "--version",
        action="version",
        version=f"hive {_hive_version()}",
        help="Show installed hive CLI version.",
    )
    top = parser.add_subparsers(dest="command")

    quickstart_parser = top.add_parser("quickstart", help="Run a 60-second local smoke flow (run -> ls hint -> open hint).")
    quickstart_parser.add_argument("--no-open", action="store_true", help="Do not open local UI automatically.")
    quickstart_parser.add_argument("--ui-url", default="", help="Override local UI URL.")

    doctor_parser = top.add_parser("doctor", help="Run local preflight checks for hive trace.")
    doctor_parser.add_argument("--ui-url", default="", help="Optional UI URL to check (default: http://localhost:5173).")

    trace_parser = top.add_parser("trace", help="Trace harness commands.")
    trace_sub = trace_parser.add_subparsers(dest="trace_command")

    run_parser = trace_sub.add_parser("run", help="Run a command with trace capture.")
    run_parser.add_argument("--name", dest="run_name", default="", help="Optional run label.")
    run_parser.add_argument("--cwd", default="", help="Working directory for the command.")
    run_parser.add_argument("--timeout-sec", type=int, default=0, help="Command timeout in seconds.")
    run_parser.add_argument("--no-open", action="store_true", help="Do not open local UI automatically.")
    run_parser.add_argument("--ui-url", default="", help="Override local UI URL.")
    run_parser.add_argument("--proxy", action="store_true", help="Enable OpenAI-compatible local proxy capture.")
    run_parser.add_argument(
        "--proxy-upstream",
        default="",
        help="Upstream OpenAI-compatible base URL (default: OPENAI_BASE_URL env or https://api.openai.com/v1).",
    )
    run_parser.add_argument("--proxy-port", type=int, default=0, help="Local proxy port (default: auto).")
    run_parser.add_argument("argv", nargs=argparse.REMAINDER, help="Command after '--', e.g. -- python agent.py")

    ls_parser = trace_sub.add_parser("ls", help="List recent traced runs.")
    ls_parser.add_argument("--limit", type=int, default=30, help="Maximum runs to list.")
    ls_parser.add_argument("--json", action="store_true", help="Print run records as JSON lines.")

    show_parser = trace_sub.add_parser("show", help="Show detailed events for one run.")
    show_parser.add_argument("run_id", help="Run ID to inspect.")
    show_parser.add_argument("--limit", type=int, default=200, help="Maximum events to show.")
    show_parser.add_argument("--json", action="store_true", help="Print event documents as JSON lines.")

    open_parser = trace_sub.add_parser("open", help="Open a run in local UI.")
    open_parser.add_argument("run_id", help="Run ID to open.")
    open_parser.add_argument("--ui-url", default="", help="Override local UI URL.")

    # Backward compatibility aliases (pre-alpha surface).
    legacy_run = top.add_parser("run", help=argparse.SUPPRESS)
    legacy_run.add_argument("--name", dest="run_name", default="")
    legacy_run.add_argument("--cwd", default="")
    legacy_run.add_argument("--timeout-sec", type=int, default=0)
    legacy_run.add_argument("argv", nargs=argparse.REMAINDER)

    legacy_trace = top.add_parser("trace-events", help=argparse.SUPPRESS)
    legacy_trace.add_argument("--run-id", default="")
    legacy_trace.add_argument("--event-type", default="")
    legacy_trace.add_argument("--outcome", default="")
    legacy_trace.add_argument("--agent-id", default="")
    legacy_trace.add_argument("--task-id", default="")
    legacy_trace.add_argument("--chunk-id", default="")
    legacy_trace.add_argument("--zone-id", default="")
    legacy_trace.add_argument("--limit", type=int, default=50)
    legacy_trace.add_argument("--json", action="store_true")
    return parser


def _print_trace_rows(events):
    if not events:
        print("No trace events matched.")
        return
    for event in events:
        ts = str(event.get("timestamp") or "-")
        outcome = str(event.get("outcome") or "-")
        event_type = str(event.get("event_type") or "-")
        run_id = str(event.get("run_id") or "-")
        payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
        step_name = str(payload.get("step_name") or payload.get("run_name") or "").strip()
        suffix = f" | {step_name}" if step_name else ""
        print(f"{ts} | {outcome:<7} | {event_type:<22} | {run_id}{suffix}")


def _handle_legacy_trace_events(args):
    limit = max(1, min(1000, int(args.limit or 50)))
    filters = {
        "run_id": str(args.run_id or "").strip() or None,
        "event_type": str(args.event_type or "").strip() or None,
        "outcome": str(args.outcome or "").strip() or None,
        "agent_id": str(args.agent_id or "").strip() or None,
        "task_id": str(args.task_id or "").strip() or None,
        "chunk_id": str(args.chunk_id or "").strip() or None,
        "zone_id": str(args.zone_id or "").strip() or None,
    }
    events = read_trace_events(limit=limit, filters=filters)
    if args.json:
        for event in events:
            print(json.dumps(event, separators=(",", ":")))
    else:
        _print_trace_rows(events)
    return 0


def _build_ui_url(base_url, run_id):
    base = str(base_url or "").strip() or "http://localhost:5173"
    joiner = "&" if "?" in base else "?"
    return f"{base}{joiner}run_id={quote_plus(str(run_id))}"


def _open_run_in_ui(base_url, run_id):
    if str(os.environ.get("HIVE_TRACE_DISABLE_BROWSER") or "").strip() in {"1", "true", "yes"}:
        return _build_ui_url(base_url, run_id)
    url = _build_ui_url(base_url, run_id)
    try:
        webbrowser.open(url, new=2)
    except Exception:
        pass
    return url


def _handle_trace_run(args):
    raw = list(args.argv or [])
    if raw and raw[0] == "--":
        raw = raw[1:]
    if not raw:
        raise SystemExit("No command provided. Example: hive trace run -- python agent.py")
    run_id = _build_run_id("observe-run")
    ui_url = ""
    if not bool(args.no_open):
        ui_url = str(args.ui_url or os.environ.get("HIVE_TRACE_UI_URL") or "http://localhost:5173").strip()
    proxy_enabled = bool(args.proxy)
    proxy_upstream = str(
        args.proxy_upstream
        or os.environ.get("HIVE_TRACE_PROXY_UPSTREAM")
        or os.environ.get("OPENAI_BASE_URL")
        or "https://api.openai.com/v1"
    ).strip()
    env_overrides = {}
    proxy_context = None
    if proxy_enabled:
        proxy_context = observe_proxy(
            run_id=run_id,
            upstream_base=proxy_upstream,
            bind_host="127.0.0.1",
            bind_port=int(args.proxy_port or 0),
            agent_id="observer.proxy",
        )
    if proxy_context is None:
        result = run_command_observed(
            raw,
            run_id=run_id,
            run_name=args.run_name or None,
            cwd=args.cwd or None,
            timeout_sec=args.timeout_sec or None,
            agent_id="observer.cli",
            open_ui_url=ui_url,
            env_overrides=env_overrides,
        )
    else:
        with proxy_context as proxy_server:
            env_overrides = _build_proxy_env(proxy_server.url)
            result = run_command_observed(
                raw,
                run_id=run_id,
                run_name=args.run_name or None,
                cwd=args.cwd or None,
                timeout_sec=args.timeout_sec or None,
                agent_id="observer.cli",
                open_ui_url=ui_url,
                env_overrides=env_overrides,
            )
            result["proxy_url"] = proxy_server.url
    print(f"run_id={result['run_id']} status={result['status']} duration_ms={result['duration_ms']}")
    if result.get("proxy_url"):
        print(f"proxy={result['proxy_url']}")
    if ui_url:
        print(f"ui={_build_ui_url(ui_url, result['run_id'])}")
    return int(result["exit_code"])


def _format_age_ms(age_ms):
    sec = max(0, int(age_ms / 1000))
    if sec < 60:
        return f"{sec}s"
    mins = sec // 60
    if mins < 60:
        return f"{mins}m"
    hrs = mins // 60
    if hrs < 24:
        return f"{hrs}h"
    return f"{hrs // 24}d"


def _handle_trace_ls(args):
    records = _list_run_records(limit=args.limit)
    if args.json:
        for record in records:
            print(json.dumps(record, separators=(",", ":")))
        return 0
    if not records:
        print("No traced runs found.")
        return 0
    now = _now_ms()
    for rec in records:
        run_id = str(rec.get("run_id") or "-")
        status = str(rec.get("status") or "unknown")
        started = int(rec.get("started_at_ms") or 0)
        age = _format_age_ms(now - started) if started > 0 else "--"
        command = _shorten(rec.get("command"), limit=100)
        print(f"{run_id} | {status:<7} | {age:>4} | {command}")
    return 0


def _handle_trace_show(args):
    run_id = str(args.run_id or "").strip()
    if not run_id:
        raise SystemExit("run_id is required")
    limit = max(1, min(1000, int(args.limit or 200)))
    record = _read_run_record(run_id) or {}
    preferred_log = str(record.get("trace_log_path") or "").strip()
    events = _read_trace_events_for_run(run_id, limit=limit, trace_log_path=preferred_log)
    if not events:
        # Backward-compatible fallback for older runs without trace_log_path.
        events = read_trace_events(limit=limit, filters={"run_id": run_id})
    if args.json:
        for event in events:
            print(json.dumps(event, separators=(",", ":")))
        return 0
    _print_trace_rows(events)
    return 0


def _handle_trace_open(args):
    run_id = str(args.run_id or "").strip()
    if not run_id:
        raise SystemExit("run_id is required")
    record = _read_run_record(run_id)
    if not record:
        raise SystemExit(f"run_id not found: {run_id}")
    base_url = str(args.ui_url or os.environ.get("HIVE_TRACE_UI_URL") or "http://localhost:5173").strip()
    url = _open_run_in_ui(base_url, run_id)
    print(f"opened={url}")
    return 0


def _check_python_runtime():
    major, minor = sys.version_info[:2]
    ok = (major, minor) >= (3, 10)
    msg = f"Python {major}.{minor}"
    if not ok:
        msg += " (requires >= 3.10)"
    return ok, msg


def _check_trace_home():
    try:
        root = _trace_home()
        root.mkdir(parents=True, exist_ok=True)
        probe = root / ".write_probe"
        probe.write_text("ok", encoding="utf-8")
        probe.unlink(missing_ok=True)
        return True, f"trace home writable: {root}"
    except Exception as exc:
        return False, f"trace home not writable: {_shorten(exc, limit=200)}"


def _check_ui_reachable(ui_url):
    url = str(ui_url or "").strip() or "http://localhost:5173"
    host = "localhost"
    port = 5173
    try:
        stripped = url.split("://", 1)[-1]
        host_port = stripped.split("/", 1)[0]
        if ":" in host_port:
            host, raw_port = host_port.rsplit(":", 1)
            port = int(raw_port)
        else:
            host = host_port
            port = 80
    except Exception:
        host, port = "localhost", 5173
    try:
        with socket.create_connection((host, port), timeout=1.2):
            return True, f"ui reachable: {host}:{port}"
    except Exception:
        return False, f"ui unreachable: {host}:{port}"


def _handle_doctor(args):
    checks = [
        ("python", *_check_python_runtime()),
        ("trace_home", *_check_trace_home()),
        ("ui", *_check_ui_reachable(args.ui_url)),
    ]
    all_ok = True
    for name, ok, message in checks:
        status = "OK" if ok else "WARN"
        print(f"[{status}] {name}: {message}")
        if not ok and name in {"python", "trace_home"}:
            all_ok = False
    return 0 if all_ok else 2


def _handle_quickstart(args):
    ui_url = ""
    if not bool(args.no_open):
        ui_url = str(args.ui_url or os.environ.get("HIVE_TRACE_UI_URL") or "http://localhost:5173").strip()
    result = run_command_observed(
        ["python", "-c", "print('hive-trace quickstart ok')"],
        run_name="hive-quickstart",
        agent_id="observer.quickstart",
        open_ui_url=ui_url,
    )
    print("quickstart_complete=true")
    print(f"run_id={result['run_id']}")
    print("next=hive trace ls")
    print(f"next=hive trace show {result['run_id']}")
    if ui_url:
        print(f"next=hive trace open {result['run_id']} --ui-url {ui_url}")
    return int(result["exit_code"])


def main(argv=None):
    parser = _build_parser()
    args = parser.parse_args(argv)

    if args.command == "quickstart":
        return _handle_quickstart(args)
    if args.command == "doctor":
        return _handle_doctor(args)

    if args.command == "trace":
        if args.trace_command == "run":
            return _handle_trace_run(args)
        if args.trace_command == "ls":
            return _handle_trace_ls(args)
        if args.trace_command == "show":
            return _handle_trace_show(args)
        if args.trace_command == "open":
            return _handle_trace_open(args)
        parser.print_help()
        return 1

    # Legacy compatibility: `hive run -- ...`
    if args.command == "run":
        class _CompatArgs:
            run_name = args.run_name
            cwd = args.cwd
            timeout_sec = args.timeout_sec
            no_open = False
            ui_url = str(os.environ.get("HIVE_TRACE_UI_URL") or "http://localhost:5173")
            argv = args.argv

        return _handle_trace_run(_CompatArgs())

    # Legacy trace inspector
    if args.command == "trace-events":
        return _handle_legacy_trace_events(args)

    parser.print_help()
    return 1


if __name__ == "__main__":
    raise SystemExit(main())
    run_id = _build_run_id("observe-run")
