"""AgentReady Trace SDK — single-file, stdlib only.

Download / vendor this file. No external dependencies.

Basic usage:
    from agentready_runtime_trace import AgentReadyTrace
    tr = AgentReadyTrace(
        base_url="https://<your-agentready-host>",
        api_key="<your-report-access-token>",
        report_id="rpt_...",
    )
    tr.record("checkout-agent", "tool_call", {"tool": "refund", "amount": 42.0},
              input_ref="session-abc", tool_calls=[{"tool": "refund", "ok": True}])
    tr.flush()   # POSTs {"report_id":..., "rows":[...]} with X-AgentReady-Key

Fails soft on network errors (drops the batch; never raises to caller).
"""

from __future__ import annotations

import json
import urllib.error
import urllib.request
from datetime import datetime, timezone
from typing import Any

__version__ = "0.1.0"


class AgentReadyTrace:
    """Buffered trace emitter for the AgentReady /api/trace surface."""

    def __init__(
        self,
        *,
        base_url: str,
        api_key: str,
        report_id: str,
        max_buffer: int = 2000,
    ) -> None:
        self.base_url = (base_url or "").rstrip("/")
        self.api_key = api_key or ""
        self.report_id = report_id
        self.max_buffer = int(max_buffer)
        self._buffer: list[dict[str, Any]] = []

    def _now(self) -> str:
        return datetime.now(timezone.utc).isoformat()

    def record(
        self,
        agent: str,
        decision_type: str,
        decision: Any,
        *,
        tool_calls: list[dict[str, Any]] | None = None,
        input_ref: str | None = None,
        model_class: str | None = None,
        human_touched: bool = False,
        experiment_arm: str | None = None,
        outcome: str | None = None,
        cost_usd: float = 0.0,
    ) -> dict[str, Any]:
        """Append one schema-shaped row to the buffer.

        ``decision`` becomes ``decision_json`` (dicts pass through; other values
        are lightly wrapped so the row remains schema-valid).
        """
        dj = decision if isinstance(decision, dict) else (
            decision if decision is None else {"value": decision}
        )
        row: dict[str, Any] = {
            "ts": self._now(),
            "decision_id": None,
            "agent": str(agent),
            "decision_type": str(decision_type),
            "input_ref": input_ref,
            "decision_json": dj,
            "model_class": model_class,
            "tool_calls": list(tool_calls) if tool_calls else [],
            "human_touched": bool(human_touched),
            "experiment_arm": experiment_arm or "none",
            "outcome": outcome,
            "cost_usd": float(cost_usd),
        }
        self._buffer.append(row)
        if len(self._buffer) > self.max_buffer:
            # bounded growth under bursty agents
            self._buffer = self._buffer[-self.max_buffer:]
        return row

    def flush(self) -> dict[str, Any]:
        """POST buffered rows (if any) to /api/trace in batches of 50. Fail-soft."""
        if not self._buffer:
            return {"accepted": 0, "rejected": []}

        batch_all = self._buffer[:]
        self._buffer.clear()

        url = f"{self.base_url}/api/trace"
        accepted = 0
        rejected: list[Any] = []
        # The server caps a request at 50 rows; chunk locally so nothing is refused.
        for i in range(0, len(batch_all), 50):
            batch = batch_all[i:i + 50]
            payload = {"report_id": self.report_id, "rows": batch}
            data = json.dumps(payload, default=str).encode("utf-8")
            req = urllib.request.Request(
                url,
                data=data,
                headers={
                    "Content-Type": "application/json",
                    "X-AgentReady-Key": self.api_key,
                },
                method="POST",
            )
            try:
                with urllib.request.urlopen(req, timeout=15) as resp:
                    body = resp.read().decode("utf-8", errors="replace")
                    try:
                        out = json.loads(body)
                        accepted += int(out.get("accepted") or 0)
                        rejected.extend(out.get("rejected") or [])
                    except (ValueError, TypeError):
                        accepted += len(batch)
            except (urllib.error.URLError, OSError, TimeoutError):
                # Fail soft — production agents must never crash on telemetry.
                rejected.append("network error; batch dropped (fail-soft)")
        return {"accepted": accepted, "rejected": rejected}


__all__ = ["AgentReadyTrace", "__version__"]
