"""
Subprocess runtime for one plugin.

Executed as: python -m plugin_runtime_process --plugin-dir ... --plugin-slug ...
"""
from __future__ import annotations

import argparse
import collections.abc
import importlib.util
import json
import os
import subprocess
import sys
import threading
import time
from dataclasses import dataclass
from typing import Any, Callable

from plugin_ipc_protocol import (
    ACTION_CANCEL_REQUEST,
    ACTION_CONTROL_EVENT,
    ACTION_HANDSHAKE,
    ACTION_HEALTH,
    ACTION_INVOKE_HOOK,
    ACTION_INVOKE_TOOL,
    ACTION_ROUTE_REQUEST,
    ACTION_SYNC_SETTINGS,
    EVENT_CANCEL_ACK,
    EVENT_TOOL_STREAM_CHUNK,
    EVENT_TOOL_STREAM_FINAL,
    EVENT_TOOL_STREAM_PROGRESS,
    IpcError,
    IpcErrorCode,
    decode_frame,
    encode_frame,
    make_event,
    make_response,
    protocol_is_compatible,
)


@dataclass
class ToolDef:
    name: str
    description: str
    args_schema: dict[str, Any]
    metadata: dict[str, Any]
    raw: Any


class RuntimePluginContext:
    def __init__(
        self,
        plugin_name: str,
        plugin_slug: str,
        auth: str,
        *,
        emit_frame: Callable[[dict[str, Any]], None],
    ):
        self._name = plugin_name
        self._slug = plugin_slug
        self._auth = auth
        self._tools: list[ToolDef] = []
        self._hooks: dict[str, list] = {}
        self._live_view_renderers: dict[str, dict] = {}
        self._settings_schema: dict | None = None
        self._settings_cache: dict[str, Any] = {}
        self._route_handler = None
        self._control_handler = None
        self._warnings: list[str] = []
        self._emit_frame = emit_frame
        self._dashboard_tiles: list[dict[str, Any]] = []

    def _emit_event(self, event: str, payload: dict[str, Any]) -> None:
        frame = make_event(
            plugin_id=self._slug,
            auth=self._auth,
            event=event,
            payload=payload,
            seq=0,
        )
        self._emit_frame(frame)

    def register_tools(self, tools: list) -> None:
        for t in tools or []:
            name = str(getattr(t, "name", "") or "").strip()
            if not name:
                continue
            desc = str(getattr(t, "description", "") or "")
            meta = getattr(t, "metadata", None)
            metadata = dict(meta) if isinstance(meta, dict) else {}
            args_schema_obj = getattr(t, "args_schema", None)
            args_schema: dict[str, Any] = {}
            if args_schema_obj is not None:
                try:
                    if hasattr(args_schema_obj, "model_json_schema"):
                        args_schema = args_schema_obj.model_json_schema()
                    elif hasattr(args_schema_obj, "schema"):
                        args_schema = args_schema_obj.schema()
                except Exception:
                    args_schema = {}
            self._tools.append(
                ToolDef(
                    name=name,
                    description=desc,
                    args_schema=args_schema if isinstance(args_schema, dict) else {},
                    metadata=metadata,
                    raw=t,
                )
            )

    def log(self, message: str, level: str = "INFO") -> None:
        sys.stderr.write(f"[PLUGIN:{self._name}] {level} {message}\n")
        sys.stderr.flush()

    def emit_live_view_event(self, event: dict) -> None:
        payload = dict(event or {})
        payload["source"] = f"plugin:{self._name}"
        self._emit_event("live_view_event", payload)

    def register_live_view_renderer(self, event_type: str, renderer: dict) -> None:
        prefix = f"plugin_{self._slug}_"
        if not str(event_type or "").startswith(prefix):
            raise ValueError(f"event type must start with '{prefix}'")
        self._live_view_renderers[str(event_type)] = dict(renderer or {})

    def register_routes(self, setup_fn) -> None:
        self._warnings.append("register_routes() is not supported in sandbox mode; use register_route_handler().")

    def register_route_handler(self, handler) -> None:
        if callable(handler):
            self._route_handler = handler
        else:
            self._warnings.append("register_route_handler() ignored non-callable input.")

    def register_dashboard_tile(self, spec: dict[str, Any]) -> None:
        """Declare a tile for the /dashboard overlay (dashboard plugins only)."""
        if not isinstance(spec, dict):
            self._warnings.append("register_dashboard_tile() requires a dict")
            return
        title = str(spec.get("title") or "").strip()
        path = str(spec.get("path") or "/").strip()
        if not path.startswith("/"):
            path = "/" + path
        entry = {
            "title": title or self._name,
            "path": path,
            "id": str(spec.get("id") or title or self._slug).strip(),
        }
        rs = spec.get("refresh_seconds")
        if isinstance(rs, (int, float)) and rs > 0:
            entry["refresh_seconds"] = int(rs)
        pr = spec.get("priority")
        if isinstance(pr, (int, float)):
            entry["priority"] = int(pr)
        layout = str(spec.get("layout") or "").strip().lower()
        if layout in {"hero", "wide", "feature"}:
            entry["layout"] = layout
        self._dashboard_tiles.append(entry)

    def register_settings(self, schema: dict) -> None:
        self._settings_schema = dict(schema or {})
        fields = self._settings_schema.get("fields")
        if isinstance(fields, list):
            for f in fields:
                if not isinstance(f, dict):
                    continue
                key = str(f.get("key") or "").strip()
                if key and "default" in f and key not in self._settings_cache:
                    self._settings_cache[key] = f.get("default")

    def get_setting(self, key: str, default=None):
        return self._settings_cache.get(key, default)

    def set_setting(self, key: str, value) -> None:
        k = str(key or "").strip()
        if not k:
            return
        self._settings_cache[k] = value
        self._emit_event("settings_set", {"key": k, "value": value})

    def register_hook(self, event_name: str, callback) -> None:
        valid = (
            "on_agent_start",
            "on_tool_call",
            "on_tool_result",
            "on_tool_call_blocked",
            "on_agent_done",
        )
        if event_name not in valid:
            self._warnings.append(f"unknown hook '{event_name}'")
            return
        if callable(callback):
            self._hooks.setdefault(event_name, []).append(callback)

    # -- Stream / control APIs (mirror PluginContext in plugin_manager.py) ------

    def _emit_stream_event(self, event: dict[str, Any]) -> None:
        evt = dict(event or {})
        # Swift PluginDashboardView and mugi_backend stream meta use plugin_id (same as in-process PluginContext).
        evt.setdefault("plugin_id", self._slug)
        evt.setdefault("plugin_slug", self._slug)
        evt.setdefault("plugin_name", self._name)
        evt.setdefault("timestamp_ms", int(time.time() * 1000))
        self._emit_event("stream_event", evt)

    def open_stream(
        self,
        stream_id: str | None = None,
        *,
        stream_type: str = "timeseries",
        title: str | None = None,
        metadata: dict | None = None,
        thread_id: str | None = None,
    ) -> str:
        sid = (stream_id or "").strip() or f"{self._slug}_{os.urandom(6).hex()}"
        self._emit_stream_event(
            {"event": "open", "stream_id": sid, "stream_type": stream_type,
             "title": title or sid, "metadata": metadata or {}, "thread_id": thread_id}
        )
        return sid

    def push_stream_update(
        self, stream_id: str, payload: dict, *, thread_id: str | None = None,
    ) -> None:
        sid = (stream_id or "").strip()
        if sid:
            self._emit_stream_event(
                {"event": "update", "stream_id": sid, "data": payload or {}, "thread_id": thread_id}
            )

    def close_stream(
        self, stream_id: str, *, status: str = "completed",
        payload: dict | None = None, thread_id: str | None = None,
    ) -> None:
        sid = (stream_id or "").strip()
        if sid:
            self._emit_stream_event(
                {"event": "close", "stream_id": sid, "status": status,
                 "data": payload or {}, "thread_id": thread_id}
            )

    def emit_stream_error(
        self, stream_id: str, message: str, *,
        payload: dict | None = None, thread_id: str | None = None,
    ) -> None:
        sid = (stream_id or "").strip()
        if sid:
            self._emit_stream_event(
                {"event": "error", "stream_id": sid, "message": message,
                 "data": payload or {}, "thread_id": thread_id}
            )

    def register_stream_controls(
        self, stream_id: str, controls: list[dict], *, thread_id: str | None = None,
    ) -> list[dict]:
        sid = (stream_id or "").strip()
        if not sid:
            return []
        self._emit_stream_event(
            {"event": "controls", "stream_id": sid,
             "data": {"controls": controls or []}, "thread_id": thread_id}
        )
        return controls or []

    def register_control_handler(self, callback) -> None:
        if callable(callback):
            self._control_handler = callback

    # -- Path safety helpers ---------------------------------------------------

    def resolve_safe_path(self, path: str, allow_write: bool = False) -> tuple[str | None, str]:
        try:
            from utils.path_safety import safe_resolve_path
            from core import get_workspace_path
            return safe_resolve_path(get_workspace_path(), path, allow_write=allow_write)
        except Exception as e:
            return None, str(e)

    def is_path_allowed(self, path: str) -> bool:
        try:
            from utils.path_safety import is_path_allowed
            return is_path_allowed(path)
        except Exception:
            return False

    # -- User decision IPC (forwarded to host) ---------------------------------

    def request_user_decision(
        self, kind: str, *, context: dict | None = None,
        timeout_s: float = 300.0, thread_id: str | None = None,
    ) -> str | None:
        try:
            from user_decision_ipc import request_user_decision as _req_ud
            return _req_ud(kind=kind, context=context or {}, timeout_s=timeout_s)
        except Exception:
            return None

    def tool_by_name(self, name: str):
        n = str(name or "").strip()
        if not n:
            return None
        for t in self._tools:
            if t.name == n:
                return t
        collapsed = n.replace("__", "_")
        for t in self._tools:
            if t.name.replace("__", "_") == collapsed:
                return t
        return None

    def bootstrap_payload(self) -> dict[str, Any]:
        return {
            "plugin_name": self._name,
            "plugin_slug": self._slug,
            "tools": [
                {
                    "name": t.name,
                    "description": t.description,
                    "args_schema": t.args_schema,
                    "metadata": t.metadata,
                }
                for t in self._tools
            ],
            "hooks": sorted(self._hooks.keys()),
            "settings_schema": self._settings_schema,
            "live_view_renderers": dict(self._live_view_renderers),
            "route_mode": "handler" if self._route_handler else "none",
            "warnings": list(self._warnings),
            "dashboard_tiles": list(self._dashboard_tiles),
        }


def _tool_like_name(cmd) -> str:
    if isinstance(cmd, (list, tuple)) and cmd:
        return os.path.basename(str(cmd[0]))
    if isinstance(cmd, str):
        return os.path.basename(cmd.split(" ", 1)[0])
    return ""


_SUBPROCESS_ORIGINALS: tuple[Any, ...] | None = None


def _install_subprocess_guard(capabilities: dict[str, Any]) -> None:
    """Replace subprocess entry points; always rebuilds from real stdlib originals."""
    global _SUBPROCESS_ORIGINALS
    if _SUBPROCESS_ORIGINALS is None:
        _SUBPROCESS_ORIGINALS = (
            subprocess.Popen,
            subprocess.run,
            subprocess.call,
            subprocess.check_output,
        )
    _orig_popen, _orig_run, _orig_call, _orig_check_output = _SUBPROCESS_ORIGINALS
    shell_allowed = bool(capabilities.get("shell", False))
    allowlist = {str(x).strip() for x in (capabilities.get("shell_allowlist") or []) if str(x).strip()}

    def _enforce(cmd):
        if not shell_allowed:
            raise PermissionError("runtime capability denied subprocess execution")
        name = _tool_like_name(cmd)
        if allowlist and name and name not in allowlist:
            raise PermissionError(f"subprocess executable '{name}' is not in shell allowlist")

    def _guarded_popen(*args, **kwargs):
        _enforce(args[0] if args else kwargs.get("args"))
        return _orig_popen(*args, **kwargs)

    def _guarded_run(*args, **kwargs):
        _enforce(args[0] if args else kwargs.get("args"))
        return _orig_run(*args, **kwargs)

    def _guarded_call(*args, **kwargs):
        _enforce(args[0] if args else kwargs.get("args"))
        return _orig_call(*args, **kwargs)

    def _guarded_check_output(*args, **kwargs):
        _enforce(args[0] if args else kwargs.get("args"))
        return _orig_check_output(*args, **kwargs)

    subprocess.Popen = _guarded_popen  # type: ignore[assignment]
    subprocess.run = _guarded_run  # type: ignore[assignment]
    subprocess.call = _guarded_call  # type: ignore[assignment]
    subprocess.check_output = _guarded_check_output  # type: ignore[assignment]


def _error_response(
    req: dict[str, Any], code: str, message: str, *, retryable: bool = False, seq: int | None = None
) -> dict[str, Any]:
    return make_response(
        request=req,
        ok=False,
        error=IpcError(code=code, message=message, retryable=retryable),
        seq=int(seq or 0),
    )


def _load_plugin(
    plugin_dir: str,
    plugin_slug: str,
    plugin_name: str,
    auth: str,
    *,
    emit_frame: Callable[[dict[str, Any]], None],
) -> RuntimePluginContext:
    plugin_py = os.path.join(plugin_dir, "plugin.py")
    if not os.path.isfile(plugin_py):
        raise RuntimeError("Missing plugin.py")
    qualname = f"mugi_plugin_runtime_{plugin_slug}"
    spec = importlib.util.spec_from_file_location(qualname, plugin_py)
    if not spec or not spec.loader:
        raise RuntimeError(f"Cannot load plugin from {plugin_py}")
    mod = importlib.util.module_from_spec(spec)
    sys.modules[qualname] = mod
    spec.loader.exec_module(mod)
    register_fn = getattr(mod, "register", None)
    if not callable(register_fn):
        raise RuntimeError("No register() function")
    ctx = RuntimePluginContext(
        plugin_name=plugin_name,
        plugin_slug=plugin_slug,
        auth=auth,
        emit_frame=emit_frame,
    )
    register_fn(ctx)
    return ctx


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--plugin-dir", required=True)
    parser.add_argument("--plugin-slug", required=True)
    parser.add_argument("--plugin-name", required=True)
    parser.add_argument("--plugin-hash", required=True)
    parser.add_argument("--auth", required=True)
    parser.add_argument("--session-nonce", required=True)
    args = parser.parse_args()

    seq = 0
    write_lock = threading.Lock()

    def emit_frame(frame: dict[str, Any]) -> None:
        nonlocal seq
        with write_lock:
            seq += 1
            stamped = dict(frame)
            stamped["seq"] = seq
            sys.stdout.buffer.write(encode_frame(stamped))
            sys.stdout.buffer.flush()

    # Deny subprocess until the host sends an explicit capability handshake.
    _install_subprocess_guard({"shell": False, "shell_allowlist": []})
    ctx = _load_plugin(
        args.plugin_dir,
        args.plugin_slug,
        args.plugin_name,
        args.auth,
        emit_frame=emit_frame,
    )
    last_req_seq = 0
    last_hb = time.time()
    cancelled: set[str] = set()
    capabilities: dict[str, Any] = {
        "network": False,
        "shell": False,
        "filesystem": "none",
        "route_enable": True,
        "hooks": True,
        "stream_events": True,
    }
    _network_tool_names = {
        "search_web",
        "deep_web_search",
        "read_webpage",
        "read_and_analyze_page",
        "download_file",
        "screenshot_webpage",
    }
    _shell_tool_names = {"shell_execute", "run_python_code", "run_installed_cli", "install_cli_package"}

    while True:
        raw = sys.stdin.buffer.readline()
        if not raw:
            return 0
        try:
            req = decode_frame(raw)
        except Exception as e:
            out = _error_response(
                {"request_id": "", "plugin_id": args.plugin_slug},
                IpcErrorCode.BAD_REQUEST,
                f"invalid frame: {e}",
            )
            emit_frame(out)
            continue

        if req.get("kind") != "request":
            continue
        if not str(req.get("nonce") or "").strip():
            out = _error_response(req, IpcErrorCode.AUTH_FAILED, "missing request nonce")
            emit_frame(out)
            continue
        req_seq = int(req.get("seq") or 0)
        if req_seq > 0 and req_seq <= last_req_seq:
            out = _error_response(req, IpcErrorCode.AUTH_FAILED, "non-monotonic request seq")
            emit_frame(out)
            continue
        if req_seq > 0:
            last_req_seq = req_seq
        if str(req.get("plugin_id") or "") != args.plugin_slug:
            out = _error_response(req, IpcErrorCode.AUTH_FAILED, "plugin id mismatch")
            emit_frame(out)
            continue
        if str(req.get("auth") or "") != args.auth:
            out = _error_response(req, IpcErrorCode.AUTH_FAILED, "invalid auth token")
            emit_frame(out)
            continue
        if not protocol_is_compatible(int(req.get("v") or 0)):
            out = _error_response(req, IpcErrorCode.PROTOCOL_MISMATCH, "protocol version mismatch")
            emit_frame(out)
            continue

        cap_ctx = req.get("capability_context") if isinstance(req.get("capability_context"), dict) else {}
        if str(cap_ctx.get("session_nonce") or "") != args.session_nonce:
            out = _error_response(req, IpcErrorCode.AUTH_FAILED, "session nonce mismatch")
            emit_frame(out)
            continue

        action = str(req.get("action") or "")
        payload = req.get("payload") if isinstance(req.get("payload"), dict) else {}
        try:
            if action == ACTION_HANDSHAKE:
                cap_raw = payload.get("capabilities") if isinstance(payload.get("capabilities"), dict) else {}
                capabilities = {
                    "network": bool(cap_raw.get("network", False)),
                    "shell": bool(cap_raw.get("shell", False)),
                    "shell_allowlist": list(cap_raw.get("shell_allowlist") or []),
                    "filesystem": str(cap_raw.get("filesystem", "none")),
                    "route_enable": bool(cap_raw.get("route_enable", True)),
                    "hooks": bool(cap_raw.get("hooks", True)),
                    "stream_events": bool(cap_raw.get("stream_events", True)),
                }
                _install_subprocess_guard(capabilities)
                body = dict(ctx.bootstrap_payload())
                body["protocol_version"] = 1
                body["plugin_hash"] = args.plugin_hash
                body["session_nonce"] = args.session_nonce
                body["pid"] = os.getpid()
                body["capabilities"] = dict(capabilities)
                out = make_response(request=req, ok=True, payload=body)
            elif action == ACTION_HEALTH:
                out = make_response(
                    request=req,
                    ok=True,
                    payload={
                        "ok": True,
                        "ts_ms": int(time.time() * 1000),
                        "pid": os.getpid(),
                        "plugin_slug": args.plugin_slug,
                    },
                )
            elif action == ACTION_CANCEL_REQUEST:
                target = str(payload.get("request_id") or "")
                if target:
                    cancelled.add(target)
                    ack = make_event(
                        plugin_id=args.plugin_slug,
                        auth=args.auth,
                        event=EVENT_CANCEL_ACK,
                        payload={"request_id": target},
                    )
                    emit_frame(ack)
                out = make_response(request=req, ok=True, payload={"ok": True})
            elif action == ACTION_INVOKE_TOOL:
                tool_name = str(payload.get("tool_name") or "")
                t = ctx.tool_by_name(tool_name)
                if t is None:
                    out = _error_response(req, IpcErrorCode.BAD_REQUEST, f"unknown tool '{tool_name}'")
                else:
                    if tool_name in _network_tool_names and not bool(capabilities.get("network", False)):
                        out = _error_response(
                            req,
                            IpcErrorCode.POLICY_DENIED,
                            f"runtime capability denied network tool '{tool_name}'",
                        )
                        emit_frame(out)
                        continue
                    if tool_name in _shell_tool_names and not bool(capabilities.get("shell", False)):
                        out = _error_response(
                            req,
                            IpcErrorCode.POLICY_DENIED,
                            f"runtime capability denied shell tool '{tool_name}'",
                        )
                        emit_frame(out)
                        continue
                    args_dict = payload.get("args") if isinstance(payload.get("args"), dict) else {}
                    try:
                        if hasattr(t.raw, "invoke"):
                            result = t.raw.invoke(args_dict)
                        else:
                            result = t.raw(**args_dict)
                        req_id = str(req.get("request_id") or "")
                        if (
                            isinstance(result, collections.abc.Iterable)
                            and not isinstance(result, (str, bytes, bytearray, dict, list, tuple, set))
                        ):
                            chunks: list[str] = []
                            idx = 0
                            stream_events = bool(capabilities.get("stream_events", True))
                            for item in result:
                                if req_id and req_id in cancelled:
                                    break
                                idx += 1
                                chunk = str(item)
                                chunks.append(chunk)
                                if stream_events:
                                    pev = make_event(
                                        plugin_id=args.plugin_slug,
                                        auth=args.auth,
                                        event=EVENT_TOOL_STREAM_PROGRESS,
                                        payload={"request_id": req_id, "index": idx},
                                    )
                                    emit_frame(pev)
                                    ev = make_event(
                                        plugin_id=args.plugin_slug,
                                        auth=args.auth,
                                        event=EVENT_TOOL_STREAM_CHUNK,
                                        payload={"request_id": req_id, "index": idx, "chunk": chunk},
                                    )
                                    emit_frame(ev)
                            if stream_events:
                                final_ev = make_event(
                                    plugin_id=args.plugin_slug,
                                    auth=args.auth,
                                    event=EVENT_TOOL_STREAM_FINAL,
                                    payload={"request_id": req_id, "count": idx},
                                )
                                emit_frame(final_ev)
                            result = "".join(chunks)
                        out = make_response(request=req, ok=True, payload={"result": result})
                    except Exception as e:
                        out = _error_response(req, IpcErrorCode.INTERNAL, f"tool invoke failed: {e}")
            elif action == ACTION_SYNC_SETTINGS:
                values = payload.get("settings") if isinstance(payload.get("settings"), dict) else {}
                for k, v in values.items():
                    key = str(k or "").strip()
                    if key:
                        ctx._settings_cache[key] = v
                out = make_response(request=req, ok=True, payload={"count": len(values)})
            elif action == ACTION_INVOKE_HOOK:
                if not bool(capabilities.get("hooks", True)):
                    out = _error_response(
                        req, IpcErrorCode.POLICY_DENIED, "runtime capability denied hooks"
                    )
                    emit_frame(out)
                    continue
                event_name = str(payload.get("event_name") or "")
                hook_args = payload.get("args") if isinstance(payload.get("args"), list) else []
                hook_kwargs = payload.get("kwargs") if isinstance(payload.get("kwargs"), dict) else {}
                for cb in list(ctx._hooks.get(event_name, [])):
                    try:
                        cb(*hook_args, **hook_kwargs)
                    except Exception as e:
                        ctx.log(f"hook {event_name} failed: {e}", level="ERROR")
                out = make_response(request=req, ok=True, payload={"ok": True})
            elif action == ACTION_ROUTE_REQUEST:
                if not bool(capabilities.get("route_enable", True)):
                    out = _error_response(
                        req, IpcErrorCode.POLICY_DENIED, "runtime capability denied routes"
                    )
                    emit_frame(out)
                    continue
                if not callable(ctx._route_handler):
                    out = _error_response(req, IpcErrorCode.BAD_REQUEST, "route handler not registered")
                else:
                    route_req = {
                        "method": str(payload.get("method") or "GET"),
                        "path": str(payload.get("path") or "/"),
                        "query": str(payload.get("query") or ""),
                        "headers": payload.get("headers") if isinstance(payload.get("headers"), dict) else {},
                        "body": str(payload.get("body") or ""),
                        "context": payload.get("context") if isinstance(payload.get("context"), dict) else {},
                    }
                    resp = ctx._route_handler(route_req)
                    if not isinstance(resp, dict):
                        resp = {
                            "status": 200,
                            "headers": {"content-type": "application/json"},
                            "body": json.dumps({"ok": True, "result": str(resp)}),
                        }
                    out = make_response(request=req, ok=True, payload={"response": resp})
            elif action == ACTION_CONTROL_EVENT:
                if not callable(ctx._control_handler):
                    out = _error_response(req, IpcErrorCode.BAD_REQUEST, "no control handler registered")
                else:
                    import inspect as _inspect
                    sig = _inspect.signature(ctx._control_handler)
                    if len(sig.parameters) >= 2:
                        result = ctx._control_handler(payload, ctx)
                    else:
                        result = ctx._control_handler(payload)
                    if not isinstance(result, dict):
                        result = {"status": "applied", "message": "ok"}
                    out = make_response(request=req, ok=True, payload=result)
            else:
                out = _error_response(req, IpcErrorCode.UNKNOWN_ACTION, f"unknown action '{action}'")
        except Exception as e:
            out = _error_response(req, IpcErrorCode.INTERNAL, str(e))

        emit_frame(out)

        now = time.time()
        if now - last_hb >= 2.0:
            hb = make_event(
                plugin_id=args.plugin_slug,
                auth=args.auth,
                event="heartbeat",
                payload={"ts_ms": int(now * 1000)},
            )
            emit_frame(hb)
            last_hb = now


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

