"""
Kevros Governance Client -- Agentic Identity Trust.

Your agent needs a verifiable identity. Kevros gives you one: cryptographically
signed, hash-chained, post-quantum attested, independently verifiable.

Usage:
    # Auto-signup (zero config -- signs up automatically on first use)
    from kevros_governance import GovernanceClient
    client = GovernanceClient(agent_id="my-agent")
    result = client.verify(action_type="trade", action_payload={...}, agent_id="my-agent")

    # Explicit key
    client = GovernanceClient(api_key="kvrs_...")

    # Async
    async with GovernanceClient(agent_id="my-agent") as client:
        result = await client.averify(action_type="trade", action_payload={...}, agent_id="my-agent")
"""

from __future__ import annotations

import asyncio
import hashlib
import json as _json
import logging
import math
import re
import time
from typing import Any, Dict, List, Optional, Union
from urllib.parse import quote

import httpx

from .auto_signup import get_or_create_api_key
from .models import (
    AttestResponse,
    BindIntentResponse,
    BundleResponse,
    GatewayHealth,
    IntentSource,
    IntentType,
    ModelParseError,
    VerifyOutcomeResponse,
    VerifyResponse,
)

logger = logging.getLogger("kevros.client")

DEFAULT_BASE_URL = "https://governance.taskhawktech.com"
_TIMEOUT = 30.0
_MAX_RETRIES = 3
_RETRY_BACKOFF_BASE = 0.5  # seconds; exponential: 0.5, 1.0, 2.0

# Agent IDs must be alphanumeric, hyphens, underscores, dots, and at signs.
# Maximum 256 characters. This prevents path traversal and injection.
_AGENT_ID_RE = re.compile(r"^[a-zA-Z0-9._@-]{1,256}$")

# Retriable HTTP status codes (server-side transient errors).
_RETRIABLE_STATUS_CODES = frozenset({502, 503, 504, 429})


def _validate_agent_id(agent_id: str) -> str:
    """Validate and return a safe agent ID.

    Raises ValueError if the agent_id is empty, too long, or contains
    characters that could enable path traversal or injection.
    """
    if not agent_id or not isinstance(agent_id, str):
        raise ValueError("agent_id must be a non-empty string")
    agent_id = agent_id.strip()
    if not _AGENT_ID_RE.match(agent_id):
        raise ValueError(
            "agent_id contains invalid characters or exceeds 256 chars. "
            "Allowed: alphanumeric, hyphens, underscores, dots, at signs."
        )
    return agent_id


def _validate_nonempty_str(value: Any, name: str) -> str:
    """Validate that value is a non-empty string."""
    if not value or not isinstance(value, str):
        raise ValueError(f"{name} must be a non-empty string")
    return value


def _validate_tolerance(tolerance: float) -> float:
    """Validate tolerance is a finite float in [0.0, 1.0]."""
    if not isinstance(tolerance, (int, float)) or isinstance(tolerance, bool):
        raise ValueError("tolerance must be a number")
    tolerance = float(tolerance)
    if math.isnan(tolerance) or math.isinf(tolerance):
        raise ValueError("tolerance must be a finite number")
    if not 0.0 <= tolerance <= 1.0:
        raise ValueError("tolerance must be between 0.0 and 1.0")
    return tolerance


class GovernanceError(Exception):
    """Raised when the governance gateway returns an error."""

    def __init__(self, status_code: int, detail: str):
        self.status_code = status_code
        self.detail = detail
        super().__init__(f"HTTP {status_code}: {detail}")


class CapExceededError(GovernanceError):
    """Free tier call limit exceeded. Upgrade for more calls."""

    def __init__(
        self,
        detail: str,
        upgrade_url: str = "",
        tiers: Optional[Dict[str, Any]] = None,
    ):
        super().__init__(402, detail)
        self.upgrade_url = upgrade_url
        self.tiers: Dict[str, Any] = tiers if tiers is not None else {}


class GovernanceClient:
    """
    Client for Kevros Agentic Identity Trust.

    Build a verifiable identity for your agent: cryptographically signed decisions,
    hash-chained provenance, post-quantum attestations. Independently verifiable
    by any counterparty without contacting Kevros.

    Auto-signup: Pass agent_id instead of api_key. On first use, the client
    signs up for a free API key (100 calls/mo) and caches it at ~/.kevros/api_key.

    Args:
        api_key: Your Kevros API key (starts with kvrs_). Optional if agent_id is provided.
        agent_id: Your agent's identifier. Used for auto-signup if no api_key.
        base_url: Gateway URL. Defaults to https://governance.taskhawktech.com
        timeout: Request timeout in seconds. Defaults to 30.
        max_retries: Maximum retries for transient failures. Defaults to 3.
    """

    def __init__(
        self,
        api_key: str = "",
        agent_id: str = "",
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = _TIMEOUT,
        max_retries: int = _MAX_RETRIES,
    ):
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._max_retries = max_retries

        # Resolve API key: explicit > env var > cached > auto-signup
        if api_key and api_key.startswith("kvrs_"):
            self._api_key = api_key
        elif agent_id:
            self._api_key = get_or_create_api_key(
                agent_id=agent_id, gateway_url=self._base_url,
            )
        else:
            # Try env/cache even without agent_id
            self._api_key = get_or_create_api_key(
                agent_id="anonymous", gateway_url=self._base_url,
            )

        self._headers: Dict[str, str] = {
            "X-API-Key": self._api_key,
            "Content-Type": "application/json",
        }
        self._sync_client: Optional[httpx.Client] = None
        self._async_client: Optional[httpx.AsyncClient] = None

    @property
    def api_key(self) -> str:
        """The resolved API key."""
        return self._api_key

    def __repr__(self) -> str:
        key_hint = ""
        if self._api_key:
            key_hint = self._api_key[:8] + "..."
        return f"GovernanceClient(base_url={self._base_url!r}, api_key={key_hint!r})"

    def close(self) -> None:
        """Close underlying HTTP clients. Safe to call multiple times."""
        if self._sync_client:
            try:
                self._sync_client.close()
            except Exception:
                pass
            self._sync_client = None
        if self._async_client:
            # Cannot await in sync close; caller should use async context
            # manager for async usage. Set to None to prevent reuse.
            self._async_client = None

    async def aclose(self) -> None:
        """Async close for the async HTTP client."""
        if self._async_client:
            try:
                await self._async_client.aclose()
            except Exception:
                pass
            self._async_client = None
        if self._sync_client:
            try:
                self._sync_client.close()
            except Exception:
                pass
            self._sync_client = None

    def __del__(self) -> None:
        """Best-effort cleanup on garbage collection."""
        try:
            self.close()
        except Exception:
            pass

    # --- Context managers ---

    def __enter__(self) -> GovernanceClient:
        self._sync_client = httpx.Client(
            base_url=self._base_url,
            headers=self._headers,
            timeout=self._timeout,
        )
        return self

    def __exit__(self, *args: Any) -> None:
        if self._sync_client:
            try:
                self._sync_client.close()
            except Exception:
                pass
            self._sync_client = None

    async def __aenter__(self) -> GovernanceClient:
        self._async_client = httpx.AsyncClient(
            base_url=self._base_url,
            headers=self._headers,
            timeout=self._timeout,
        )
        return self

    async def __aexit__(self, *args: Any) -> None:
        if self._async_client:
            try:
                await self._async_client.aclose()
            except Exception:
                pass
            self._async_client = None

    # --- Internal HTTP helpers ---

    def _get_sync(self) -> httpx.Client:
        if self._sync_client is None:
            self._sync_client = httpx.Client(
                base_url=self._base_url,
                headers=self._headers,
                timeout=self._timeout,
            )
        return self._sync_client

    def _get_async(self) -> httpx.AsyncClient:
        if self._async_client is None:
            self._async_client = httpx.AsyncClient(
                base_url=self._base_url,
                headers=self._headers,
                timeout=self._timeout,
            )
        return self._async_client

    def _handle_response(self, resp: httpx.Response) -> Dict[str, Any]:
        """Parse gateway response, raising structured errors on failure."""
        if resp.status_code == 402:
            data: Dict[str, Any] = {}
            detail = "Payment required"
            try:
                data = resp.json()
                if isinstance(data, dict):
                    detail = str(data.get("detail", "Monthly call limit reached"))
                else:
                    data = {}
            except Exception:
                pass
            raise CapExceededError(
                detail=detail,
                upgrade_url=data.get("upgrade_url", ""),
                tiers=data.get("tiers", {}),
            )
        if resp.status_code >= 400:
            detail = f"Request failed (HTTP {resp.status_code})"
            try:
                err_body = resp.json()
                if isinstance(err_body, dict) and "detail" in err_body:
                    detail = str(err_body["detail"])
            except Exception:
                pass
            raise GovernanceError(resp.status_code, detail)
        # Success path: parse JSON safely
        try:
            body = resp.json()
        except Exception:
            raise GovernanceError(
                resp.status_code,
                "Gateway returned invalid JSON in response body",
            )
        if not isinstance(body, dict):
            raise GovernanceError(
                resp.status_code,
                "Gateway returned non-object JSON response",
            )
        return body

    def _sync_request_with_retry(
        self,
        method: str,
        path: str,
        json: Optional[Dict[str, Any]] = None,
    ) -> httpx.Response:
        """Execute a sync HTTP request with retry on transient failures."""
        last_exc: Optional[Exception] = None
        for attempt in range(self._max_retries):
            try:
                client = self._get_sync()
                if method == "POST":
                    resp = client.post(path, json=json)
                else:
                    resp = client.get(path)
                if resp.status_code not in _RETRIABLE_STATUS_CODES:
                    return resp
                last_exc = GovernanceError(
                    resp.status_code,
                    f"Retriable server error (attempt {attempt + 1}/{self._max_retries})",
                )
            except (httpx.ConnectError, httpx.ConnectTimeout) as exc:
                last_exc = exc
            except httpx.TimeoutException as exc:
                last_exc = exc
            if attempt < self._max_retries - 1:
                backoff = _RETRY_BACKOFF_BASE * (2 ** attempt)
                time.sleep(backoff)
        # All retries exhausted
        if isinstance(last_exc, httpx.TimeoutException):
            raise GovernanceError(0, "Request timed out after all retries")
        if isinstance(last_exc, (httpx.ConnectError, httpx.ConnectTimeout)):
            raise GovernanceError(0, "Connection failed after all retries")
        if isinstance(last_exc, GovernanceError):
            raise last_exc
        raise GovernanceError(0, "Request failed after all retries")

    async def _async_request_with_retry(
        self,
        method: str,
        path: str,
        json: Optional[Dict[str, Any]] = None,
    ) -> httpx.Response:
        """Execute an async HTTP request with retry on transient failures."""
        last_exc: Optional[Exception] = None
        for attempt in range(self._max_retries):
            try:
                client = self._get_async()
                if method == "POST":
                    resp = await client.post(path, json=json)
                else:
                    resp = await client.get(path)
                if resp.status_code not in _RETRIABLE_STATUS_CODES:
                    return resp
                last_exc = GovernanceError(
                    resp.status_code,
                    f"Retriable server error (attempt {attempt + 1}/{self._max_retries})",
                )
            except (httpx.ConnectError, httpx.ConnectTimeout) as exc:
                last_exc = exc
            except httpx.TimeoutException as exc:
                last_exc = exc
            if attempt < self._max_retries - 1:
                backoff = _RETRY_BACKOFF_BASE * (2 ** attempt)
                await asyncio.sleep(backoff)
        # All retries exhausted
        if isinstance(last_exc, httpx.TimeoutException):
            raise GovernanceError(0, "Request timed out after all retries")
        if isinstance(last_exc, (httpx.ConnectError, httpx.ConnectTimeout)):
            raise GovernanceError(0, "Connection failed after all retries")
        if isinstance(last_exc, GovernanceError):
            raise last_exc
        raise GovernanceError(0, "Request failed after all retries")

    # =========================================================================
    # VERIFY -- Action verification ($0.01)
    # =========================================================================

    def verify(
        self,
        action_type: str,
        action_payload: Dict[str, Any],
        agent_id: str,
        policy_context: Optional[Dict[str, Any]] = None,
        idempotency_key: Optional[str] = None,
    ) -> VerifyResponse:
        """
        Verify an action against policy bounds before executing it.

        Returns ALLOW (proceed), CLAMP (proceed with modified values), or DENY (stop).
        Every call is recorded in the hash-chained provenance ledger.

        Args:
            action_type: Type of action (e.g., "trade", "api_call", "motor_command").
            action_payload: The action details to verify.
            policy_context: Optional policy constraints (max_values, forbidden_keys).
            agent_id: Your agent's identifier.
            idempotency_key: Optional key for retry safety.

        Returns:
            VerifyResponse with decision, release_token, and provenance_hash.
        """
        agent_id = _validate_agent_id(agent_id)
        _validate_nonempty_str(action_type, "action_type")
        body: Dict[str, Any] = {
            "action_type": action_type,
            "action_payload": action_payload,
            "agent_id": agent_id,
        }
        if policy_context is not None:
            body["policy_context"] = policy_context
        if idempotency_key is not None:
            body["idempotency_key"] = idempotency_key

        resp = self._sync_request_with_retry("POST", "/governance/verify", json=body)
        return VerifyResponse.from_dict(self._handle_response(resp))

    async def averify(
        self,
        action_type: str,
        action_payload: Dict[str, Any],
        agent_id: str,
        policy_context: Optional[Dict[str, Any]] = None,
        idempotency_key: Optional[str] = None,
    ) -> VerifyResponse:
        """Async version of verify()."""
        agent_id = _validate_agent_id(agent_id)
        _validate_nonempty_str(action_type, "action_type")
        body: Dict[str, Any] = {
            "action_type": action_type,
            "action_payload": action_payload,
            "agent_id": agent_id,
        }
        if policy_context is not None:
            body["policy_context"] = policy_context
        if idempotency_key is not None:
            body["idempotency_key"] = idempotency_key

        resp = await self._async_request_with_retry("POST", "/governance/verify", json=body)
        return VerifyResponse.from_dict(self._handle_response(resp))

    # =========================================================================
    # ATTEST -- Provenance attestation ($0.02)
    # =========================================================================

    def attest(
        self,
        agent_id: str,
        action_description: str,
        action_payload: Dict[str, Any],
        context: Optional[Dict[str, Any]] = None,
        prior_attestation_hash: Optional[str] = None,
    ) -> AttestResponse:
        """
        Create a hash-chained provenance record for an action you've taken.

        Each attestation extends the append-only evidence chain.
        The hash can be independently verified by any third party.

        Args:
            agent_id: Your agent's identifier.
            action_description: What you did (human-readable).
            action_payload: Full action details for the record.
            context: Optional additional context.
            prior_attestation_hash: Your last attestation hash (for agent-side continuity).

        Returns:
            AttestResponse with hash_prev, hash_curr, and chain_length.
        """
        agent_id = _validate_agent_id(agent_id)
        _validate_nonempty_str(action_description, "action_description")
        body: Dict[str, Any] = {
            "agent_id": agent_id,
            "action_description": action_description,
            "action_payload": action_payload,
        }
        if context is not None:
            body["context"] = context
        if prior_attestation_hash is not None:
            body["prior_attestation_hash"] = prior_attestation_hash

        resp = self._sync_request_with_retry("POST", "/governance/attest", json=body)
        return AttestResponse.from_dict(self._handle_response(resp))

    async def aattest(
        self,
        agent_id: str,
        action_description: str,
        action_payload: Dict[str, Any],
        context: Optional[Dict[str, Any]] = None,
        prior_attestation_hash: Optional[str] = None,
    ) -> AttestResponse:
        """Async version of attest()."""
        agent_id = _validate_agent_id(agent_id)
        _validate_nonempty_str(action_description, "action_description")
        body: Dict[str, Any] = {
            "agent_id": agent_id,
            "action_description": action_description,
            "action_payload": action_payload,
        }
        if context is not None:
            body["context"] = context
        if prior_attestation_hash is not None:
            body["prior_attestation_hash"] = prior_attestation_hash

        resp = await self._async_request_with_retry("POST", "/governance/attest", json=body)
        return AttestResponse.from_dict(self._handle_response(resp))

    # =========================================================================
    # BIND -- Intent binding ($0.02)
    # =========================================================================

    def bind(
        self,
        agent_id: str,
        intent_type: IntentType,
        intent_description: str,
        command_payload: Dict[str, Any],
        intent_source: IntentSource = IntentSource.AI_PLANNER,
        goal_state: Optional[Dict[str, Any]] = None,
        max_duration_ms: Optional[float] = None,
        parent_intent_id: Optional[str] = None,
    ) -> BindIntentResponse:
        """
        Declare an intent and cryptographically bind it to a command.

        Proves that the command was issued in service of the declared intent.
        Use verify_outcome() after execution to close the loop.

        Args:
            agent_id: Your agent's identifier.
            intent_type: Category of intent (NAVIGATION, COMMUNICATION, etc.).
            intent_description: What you intend to accomplish.
            command_payload: The command being bound to this intent.
            intent_source: Who/what generated the intent.
            goal_state: Target state to verify against later.
            max_duration_ms: Expected max duration.
            parent_intent_id: Parent intent for hierarchical chains.

        Returns:
            BindIntentResponse with intent_id, binding_hmac, and hashes.
        """
        agent_id = _validate_agent_id(agent_id)
        _validate_nonempty_str(intent_description, "intent_description")
        body: Dict[str, Any] = {
            "agent_id": agent_id,
            "intent_type": intent_type.value,
            "intent_description": intent_description,
            "command_payload": command_payload,
            "intent_source": intent_source.value,
        }
        if goal_state is not None:
            body["goal_state"] = goal_state
        if max_duration_ms is not None:
            body["max_duration_ms"] = max_duration_ms
        if parent_intent_id is not None:
            body["parent_intent_id"] = parent_intent_id

        resp = self._sync_request_with_retry("POST", "/governance/bind", json=body)
        return BindIntentResponse.from_dict(self._handle_response(resp))

    async def abind(
        self,
        agent_id: str,
        intent_type: IntentType,
        intent_description: str,
        command_payload: Dict[str, Any],
        intent_source: IntentSource = IntentSource.AI_PLANNER,
        goal_state: Optional[Dict[str, Any]] = None,
        max_duration_ms: Optional[float] = None,
        parent_intent_id: Optional[str] = None,
    ) -> BindIntentResponse:
        """Async version of bind()."""
        agent_id = _validate_agent_id(agent_id)
        _validate_nonempty_str(intent_description, "intent_description")
        body: Dict[str, Any] = {
            "agent_id": agent_id,
            "intent_type": intent_type.value,
            "intent_description": intent_description,
            "command_payload": command_payload,
            "intent_source": intent_source.value,
        }
        if goal_state is not None:
            body["goal_state"] = goal_state
        if max_duration_ms is not None:
            body["max_duration_ms"] = max_duration_ms
        if parent_intent_id is not None:
            body["parent_intent_id"] = parent_intent_id

        resp = await self._async_request_with_retry("POST", "/governance/bind", json=body)
        return BindIntentResponse.from_dict(self._handle_response(resp))

    # =========================================================================
    # VERIFY OUTCOME -- Outcome verification (free with bind)
    # =========================================================================

    def verify_outcome(
        self,
        agent_id: str,
        intent_id: str,
        binding_id: str,
        actual_state: Dict[str, Any],
        tolerance: float = 0.1,
    ) -> VerifyOutcomeResponse:
        """
        Verify that an executed action achieved its declared intent.

        Closes the loop: intent -> command -> action -> outcome -> verification.

        Args:
            agent_id: Your agent's identifier.
            intent_id: The intent_id from bind().
            binding_id: The binding_id from bind().
            actual_state: The state after action execution.
            tolerance: Acceptable deviation (0=exact, 1=any). Default 0.1.

        Returns:
            VerifyOutcomeResponse with status and achieved_percentage.
        """
        agent_id = _validate_agent_id(agent_id)
        _validate_nonempty_str(intent_id, "intent_id")
        _validate_nonempty_str(binding_id, "binding_id")
        tolerance = _validate_tolerance(tolerance)
        body: Dict[str, Any] = {
            "agent_id": agent_id,
            "intent_id": intent_id,
            "binding_id": binding_id,
            "actual_state": actual_state,
            "tolerance": tolerance,
        }
        resp = self._sync_request_with_retry("POST", "/governance/verify-outcome", json=body)
        return VerifyOutcomeResponse.from_dict(self._handle_response(resp))

    async def averify_outcome(
        self,
        agent_id: str,
        intent_id: str,
        binding_id: str,
        actual_state: Dict[str, Any],
        tolerance: float = 0.1,
    ) -> VerifyOutcomeResponse:
        """Async version of verify_outcome()."""
        agent_id = _validate_agent_id(agent_id)
        _validate_nonempty_str(intent_id, "intent_id")
        _validate_nonempty_str(binding_id, "binding_id")
        tolerance = _validate_tolerance(tolerance)
        body: Dict[str, Any] = {
            "agent_id": agent_id,
            "intent_id": intent_id,
            "binding_id": binding_id,
            "actual_state": actual_state,
            "tolerance": tolerance,
        }
        resp = await self._async_request_with_retry("POST", "/governance/verify-outcome", json=body)
        return VerifyOutcomeResponse.from_dict(self._handle_response(resp))

    # =========================================================================
    # BUNDLE -- Compliance package ($0.25)
    # =========================================================================

    def bundle(
        self,
        agent_id: str,
        time_range_start: Optional[str] = None,
        time_range_end: Optional[str] = None,
        include_intent_chains: bool = True,
        include_pqc_signatures: bool = True,
        include_verification_instructions: bool = True,
    ) -> BundleResponse:
        """
        Generate a certifier-grade compliance evidence bundle.

        Contains hash-chained provenance, intent bindings, PQC attestations,
        and verification instructions. Independently verifiable without Kevros access.

        Args:
            agent_id: Your agent's identifier.
            time_range_start: ISO 8601 start (default: all records).
            time_range_end: ISO 8601 end (default: now).
            include_intent_chains: Include intent binding proofs.
            include_pqc_signatures: Include PQC block references.
            include_verification_instructions: Include verification procedure.

        Returns:
            BundleResponse with records, chain_integrity, and bundle_hash.
        """
        agent_id = _validate_agent_id(agent_id)
        body: Dict[str, Any] = {
            "agent_id": agent_id,
            "include_intent_chains": include_intent_chains,
            "include_pqc_signatures": include_pqc_signatures,
            "include_verification_instructions": include_verification_instructions,
        }
        if time_range_start is not None:
            body["time_range_start"] = time_range_start
        if time_range_end is not None:
            body["time_range_end"] = time_range_end

        resp = self._sync_request_with_retry("POST", "/governance/bundle", json=body)
        return BundleResponse.from_dict(self._handle_response(resp))

    async def abundle(
        self,
        agent_id: str,
        time_range_start: Optional[str] = None,
        time_range_end: Optional[str] = None,
        include_intent_chains: bool = True,
        include_pqc_signatures: bool = True,
        include_verification_instructions: bool = True,
    ) -> BundleResponse:
        """Async version of bundle()."""
        agent_id = _validate_agent_id(agent_id)
        body: Dict[str, Any] = {
            "agent_id": agent_id,
            "include_intent_chains": include_intent_chains,
            "include_pqc_signatures": include_pqc_signatures,
            "include_verification_instructions": include_verification_instructions,
        }
        if time_range_start is not None:
            body["time_range_start"] = time_range_start
        if time_range_end is not None:
            body["time_range_end"] = time_range_end

        resp = await self._async_request_with_retry("POST", "/governance/bundle", json=body)
        return BundleResponse.from_dict(self._handle_response(resp))

    # =========================================================================
    # PEER TRUST -- Verify other agents (free, no API key needed)
    # =========================================================================

    def verify_peer(self, agent_id: str) -> Dict[str, Any]:
        """
        Check another agent's trust score and provenance history.

        Free, no API key needed. Calls GET /governance/reputation/{agent_id}.

        Args:
            agent_id: The agent to look up.

        Returns:
            dict with agent_id, trust_score, chain_length, attestation_count, etc.
        """
        agent_id = _validate_agent_id(agent_id)
        safe_id = quote(agent_id, safe="")
        resp = self._sync_request_with_retry("GET", f"/governance/reputation/{safe_id}")
        if resp.status_code == 404:
            return {"error": "not_found", "detail": f"No records for agent: {agent_id}"}
        return self._handle_response(resp)

    async def averify_peer(self, agent_id: str) -> Dict[str, Any]:
        """Async version of verify_peer()."""
        agent_id = _validate_agent_id(agent_id)
        safe_id = quote(agent_id, safe="")
        resp = await self._async_request_with_retry("GET", f"/governance/reputation/{safe_id}")
        if resp.status_code == 404:
            return {"error": "not_found", "detail": f"No records for agent: {agent_id}"}
        return self._handle_response(resp)

    def verify_peer_token(
        self,
        release_token: str,
        token_preimage: str,
    ) -> Dict[str, Any]:
        """
        Verify a release token from another agent.

        Free, no API key needed. Calls POST /governance/verify-token.

        Args:
            release_token: HMAC digest from a verify() response.
            token_preimage: The token preimage (provided alongside the release token).

        Returns:
            dict with valid, decision, epoch, chain_found.
        """
        _validate_nonempty_str(release_token, "release_token")
        _validate_nonempty_str(token_preimage, "token_preimage")
        resp = self._sync_request_with_retry("POST", "/governance/verify-token", json={
            "release_token": release_token,
            "token_preimage": token_preimage,
        })
        return self._handle_response(resp)

    async def averify_peer_token(
        self,
        release_token: str,
        token_preimage: str,
    ) -> Dict[str, Any]:
        """Async version of verify_peer_token()."""
        _validate_nonempty_str(release_token, "release_token")
        _validate_nonempty_str(token_preimage, "token_preimage")
        resp = await self._async_request_with_retry("POST", "/governance/verify-token", json={
            "release_token": release_token,
            "token_preimage": token_preimage,
        })
        return self._handle_response(resp)

    def _build_preimage(
        self,
        result: VerifyResponse,
        agent_id: str,
        action_type: str,
    ) -> str:
        """Build the token preimage from a verify result."""
        record = {
            "verification_id": result.verification_id,
            "epoch": result.epoch,
            "decision": result.decision.value,
            "agent_id": agent_id,
            "action_type": action_type,
            "timestamp_utc": result.timestamp_utc,
        }
        record_bytes = _json.dumps(
            record, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
        ).encode("utf-8")
        record_hash = hashlib.sha256(record_bytes).hexdigest()
        return f"KEVROSv1|{result.decision.value}|{result.epoch}|{result.hash_prev}|{record_hash}"

    def build_trust_headers(
        self,
        action_type: str,
        action_payload: Dict[str, Any],
        agent_id: str,
    ) -> Dict[str, str]:
        """
        Get trust headers to attach to outbound HTTP requests.

        Calls verify() to get a release token, then reconstructs the preimage
        and returns headers that downstream services can verify.

        Args:
            action_type: Type of action being taken.
            action_payload: The action details.
            agent_id: Your agent identifier.

        Returns:
            dict of HTTP headers: X-Kevros-Release-Token, X-Kevros-Token-Preimage,
            X-Kevros-Agent-Id.
        """
        result = self.verify(action_type, action_payload, agent_id)
        if result.release_token and result.hash_prev:
            preimage = self._build_preimage(result, agent_id, action_type)
            return {
                "X-Kevros-Release-Token": result.release_token,
                "X-Kevros-Token-Preimage": preimage,
                "X-Kevros-Agent-Id": agent_id,
            }
        return {"X-Kevros-Agent-Id": agent_id}

    async def abuild_trust_headers(
        self,
        action_type: str,
        action_payload: Dict[str, Any],
        agent_id: str,
    ) -> Dict[str, str]:
        """Async version of build_trust_headers()."""
        result = await self.averify(action_type, action_payload, agent_id)
        if result.release_token and result.hash_prev:
            preimage = self._build_preimage(result, agent_id, action_type)
            return {
                "X-Kevros-Release-Token": result.release_token,
                "X-Kevros-Token-Preimage": preimage,
                "X-Kevros-Agent-Id": agent_id,
            }
        return {"X-Kevros-Agent-Id": agent_id}

    def trusted_request(
        self,
        method: str,
        url: str,
        agent_id: str,
        action_type: str = "http_request",
        action_payload: Optional[Dict[str, Any]] = None,
        **kwargs: Any,
    ) -> httpx.Response:
        """
        Make an HTTP request with auto-remediation on 403 kevros_trust_required.

        If the target returns 403 with error=kevros_trust_required, automatically
        calls verify(), attaches trust headers, and retries once.

        Args:
            method: HTTP method (GET, POST, etc.).
            url: Target URL.
            agent_id: Your agent identifier.
            action_type: Action type for verify().
            action_payload: Action details for verify().
            **kwargs: Passed to httpx.request().

        Returns:
            httpx.Response from the target.
        """
        with httpx.Client(timeout=self._timeout) as c:
            resp = c.request(method, url, **kwargs)
            if resp.status_code == 403:
                try:
                    resp_body = resp.json()
                except Exception:
                    return resp
                if isinstance(resp_body, dict) and resp_body.get("error") == "kevros_trust_required":
                    trust_headers = self.build_trust_headers(
                        action_type, action_payload or {}, agent_id,
                    )
                    # Merge trust headers with any caller-provided headers
                    # without mutating the caller's kwargs dict.
                    retry_kwargs = dict(kwargs)
                    existing_headers = dict(retry_kwargs.pop("headers", None) or {})
                    existing_headers.update(trust_headers)
                    resp = c.request(method, url, headers=existing_headers, **retry_kwargs)
            return resp

    # =========================================================================
    # HEALTH -- Gateway health check (free)
    # =========================================================================

    def health(self) -> GatewayHealth:
        """Check governance gateway health."""
        resp = self._sync_request_with_retry("GET", "/governance/health")
        return GatewayHealth.from_dict(self._handle_response(resp))

    async def ahealth(self) -> GatewayHealth:
        """Async version of health()."""
        resp = await self._async_request_with_retry("GET", "/governance/health")
        return GatewayHealth.from_dict(self._handle_response(resp))
