#!/usr/bin/env python3
"""Direct ACP JSON-RPC probe for sandbox verification.

Tests the ACP contract (task/create, event/send, task/cancel) against
a running agent. Works with any sandbox executor.

Extracted from PR #241 (sandbox-sidecar-mvp).
"""

from __future__ import annotations

import argparse
import json
import time
import urllib.error
import urllib.request
import uuid
from datetime import UTC, datetime
from typing import Any


def _utc_now() -> str:
    return datetime.now(UTC).isoformat().replace("+00:00", "Z")


def _post_json(url: str, payload: dict[str, Any], headers: dict[str, str], timeout_seconds: float) -> tuple[int, str]:
    request = urllib.request.Request(
        url,
        data=json.dumps(payload).encode("utf-8"),
        headers=headers,
        method="POST",
    )
    try:
        with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
            return int(response.status), response.read().decode("utf-8", errors="replace")
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        return int(exc.code), body


def _call_jsonrpc(
    *,
    acp_url: str,
    method: str,
    params: dict[str, Any],
    request_id: str,
    agent_api_key: str,
    timeout_seconds: float,
) -> dict[str, Any]:
    status_code, body = _post_json(
        acp_url,
        {
            "jsonrpc": "2.0",
            "id": request_id,
            "method": method,
            "params": params,
        },
        {
            "content-type": "application/json",
            "x-agent-api-key": agent_api_key,
        },
        timeout_seconds,
    )

    if status_code != 200:
        raise RuntimeError(f"{method} returned HTTP {status_code}: {body}")

    try:
        payload = json.loads(body)
    except json.JSONDecodeError as exc:
        raise RuntimeError(f"{method} returned non-JSON response: {body}") from exc

    error = payload.get("error")
    if error:
        error_code = error.get("code")
        error_message = error.get("message")
        raise RuntimeError(f"{method} returned JSON-RPC error code={error_code} message={error_message}")

    return payload


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--acp-url",
        default="http://127.0.0.1:18080/api",
        help="Full ACP JSON-RPC endpoint URL (default: http://127.0.0.1:18080/api)",
    )
    parser.add_argument("--agent-api-key", required=True, help="Value for x-agent-api-key header")
    parser.add_argument("--task-id", default="", help="Optional deterministic task id")
    parser.add_argument("--event-text", default="sandbox sidecar acp probe", help="Text for event/send")
    parser.add_argument("--namespace-id", default="ns-sandbox-smoke", help="Namespace id in ACP payload")
    parser.add_argument("--agent-id", default="agt-sandbox-smoke", help="Agent id in ACP payload")
    parser.add_argument("--agent-name", default="sandbox/smoke-agent", help="Agent name in ACP payload")
    parser.add_argument("--timeout-seconds", type=float, default=20.0, help="HTTP timeout per JSON-RPC request")
    parser.add_argument(
        "--wait-seconds",
        type=float,
        default=2.0,
        help="Sleep after final request so async handlers can run before log assertions",
    )
    return parser


def main() -> int:
    args = _build_parser().parse_args()

    probe_id = uuid.uuid4().hex[:12]
    task_id = args.task_id.strip() or f"sandbox-sidecar-probe-{probe_id}"

    agent = {
        "id": args.agent_id,
        "name": args.agent_name,
        "namespace_id": args.namespace_id,
        "description": "sandbox sidecar probe agent",
        "created_at": _utc_now(),
        "updated_at": _utc_now(),
    }

    task = {
        "id": task_id,
        "namespace_id": args.namespace_id,
        "filesystem_id": "",
        "status": "RUNNING",
    }

    create_id = f"{probe_id}-create"
    event_id = f"event-{probe_id}"
    send_id = f"{probe_id}-event"
    cancel_id = f"{probe_id}-cancel"

    _call_jsonrpc(
        acp_url=args.acp_url,
        method="task/create",
        params={
            "agent": agent,
            "task": task,
            "params": {"probe_id": probe_id, "mode": "create"},
        },
        request_id=create_id,
        agent_api_key=args.agent_api_key,
        timeout_seconds=args.timeout_seconds,
    )

    _call_jsonrpc(
        acp_url=args.acp_url,
        method="event/send",
        params={
            "agent": agent,
            "task": task,
            "event": {
                "id": event_id,
                "agent_id": agent["id"],
                "task_id": task_id,
                "sequence_id": 1,
                "content": {
                    "type": "text",
                    "text": args.event_text,
                },
            },
        },
        request_id=send_id,
        agent_api_key=args.agent_api_key,
        timeout_seconds=args.timeout_seconds,
    )

    _call_jsonrpc(
        acp_url=args.acp_url,
        method="task/cancel",
        params={
            "agent": agent,
            "task": task,
        },
        request_id=cancel_id,
        agent_api_key=args.agent_api_key,
        timeout_seconds=args.timeout_seconds,
    )

    if args.wait_seconds > 0:
        time.sleep(args.wait_seconds)

    # Shell-friendly markers for downstream log assertions.
    print(f"PROBE_ID={probe_id}")
    print(f"PROBE_TASK_ID={task_id}")
    print(f"PROBE_EVENT_ID={event_id}")
    print(f"PROBE_CREATE_REQUEST_ID={create_id}")
    print(f"PROBE_EVENT_REQUEST_ID={send_id}")
    print(f"PROBE_CANCEL_REQUEST_ID={cancel_id}")
    print(f"PROBE_ACP_URL={args.acp_url}")
    print("PROBE_STATUS=ok")
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception as exc:  # pragma: no cover - CLI fallback
        print("PROBE_STATUS=error")
        print(f"PROBE_ERROR={exc}")
        raise SystemExit(1)
