"""BoBClient — main entry point for the Bank of Bots Python SDK."""

from __future__ import annotations

from typing import Any

import httpx

from .exceptions import AuthenticationError, BoBError, ForbiddenError, NotFoundError
from .types import (
    APIKeyRecord,
    Agent,
    AgentCreditResponse,
    AgentWallet,
    CompatibilityResult,
    CreditEvent,
    CreditLimitResponse,
    CurrencyBalance,
    CurrencyLimit,
    DirectoryEntry,
    HistoricalPaymentProofImport,
    InboxEvent,
    PublicAgentMessage,
    ProofCreditOutcome,
    WebhookSubscriber,
)

_SDK_VERSION = "0.46.0"
_DEFAULT_BASE_URL = "https://api.bankofbots.ai/api/v1"


def _semver_less_than(a: str, b: str) -> bool:
    """Return True if semver string *a* is less than *b*."""
    def _parse(s: str) -> list[int]:
        s = s.lstrip("v")
        parts = s.split(".")
        return [int(p) if p.isdigit() else 0 for p in (parts + ["0", "0", "0"])[:3]]

    av, bv = _parse(a), _parse(b)
    for x, y in zip(av, bv):
        if x < y:
            return True
        if x > y:
            return False
    return False


class BoBClient:
    """Client for the Bank of Bots API.

    Usage::

        client = BoBClient(api_key="bok_...", agent_id="<uuid>")
        me = client.get_me()

    Operator-scoped calls can omit ``agent_id``::

        op = BoBClient(api_key="bok_...")
        keys = op.list_api_keys()
    """

    def __init__(
        self,
        api_key: str,
        agent_id: str | None = None,
        base_url: str = _DEFAULT_BASE_URL,
        timeout: float = 30.0,
    ):
        self.api_key = api_key
        self.agent_id = agent_id
        self.base_url = base_url.rstrip("/")
        self._client = httpx.Client(
            base_url=self.base_url,
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=timeout,
        )

    @classmethod
    def register(
        cls,
        email: str,
        agent_name: str | None = None,
        platform: str = "generic",
        base_url: str = "https://api.bankofbots.ai/api/v1",
    ) -> tuple["BoBClient", dict[str, Any]]:
        """Self-register a new agent. No existing API key is required.

        Returns a tuple of ``(client, registration_response)`` where
        *client* is a fully configured :class:`BoBClient` and
        *registration_response* is a dict with keys ``agent_id``,
        ``api_key``, ``operator_id``, ``status``, and ``message``.
        """
        normalized = base_url.rstrip("/")
        payload: dict[str, Any] = {"email": email, "platform": platform}
        if agent_name:
            payload["agent_name"] = agent_name

        resp = httpx.post(
            f"{normalized}/agents/register",
            json=payload,
            headers={"Content-Type": "application/json"},
            timeout=30.0,
        )
        if resp.status_code >= 400:
            body = resp.json()
            raise BoBError(
                body.get("error", "registration failed"),
                status_code=resp.status_code,
            )

        data = resp.json()
        client = cls(
            api_key=data["api_key"],
            agent_id=data["agent_id"],
            base_url=normalized,
        )
        return client, data

    # -- Context manager --

    def __enter__(self) -> "BoBClient":
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    def close(self) -> None:
        self._client.close()

    # -- Compatibility --

    def check_compatibility(self) -> CompatibilityResult:
        """Check SDK version compatibility against the API's minimum required version.

        Calls GET /health at the server root (not under /api/v1).
        """
        import re

        origin = re.sub(r"/api/v1/?$", "", self.base_url)
        resp = httpx.get(f"{origin}/health", timeout=self._client.timeout)
        if resp.status_code >= 400:
            raise BoBError(f"Health endpoint returned {resp.status_code}", status_code=resp.status_code)
        data = resp.json()
        api_version = data.get("api_version", "")
        min_cli_version = data.get("min_cli_version", "")
        compatible = not _semver_less_than(_SDK_VERSION, min_cli_version)
        return CompatibilityResult(
            compatible=compatible,
            api_version=api_version,
            min_cli_version=min_cli_version,
            sdk_version=_SDK_VERSION,
        )

    # -- Private helpers --

    def _request(self, method: str, path: str, **kwargs: Any) -> Any:
        resp = self._client.request(method, path, **kwargs)
        if resp.status_code == 401:
            raise AuthenticationError()
        if resp.status_code == 403:
            body = resp.json()
            raise ForbiddenError(body.get("error", "access denied"))
        if resp.status_code == 404:
            body = resp.json()
            raise NotFoundError(body.get("error", "not found"))
        if resp.status_code >= 400:
            body = resp.json()
            raise BoBError(body.get("error", "request failed"), status_code=resp.status_code)
        return resp.json()

    def _agent_path(self, suffix: str = "") -> str:
        if not self.agent_id:
            raise ValueError("agent_id is required for agent-scoped endpoints")
        return f"/agents/{self.agent_id}{suffix}"

    # -- Private parse helpers --

    def _parse_proof_credit(self, data: dict[str, Any] | None) -> ProofCreditOutcome | None:
        if not isinstance(data, dict):
            return None
        known_fields = [k for k in ProofCreditOutcome.__dataclass_fields__ if k != "extra"]
        parsed = {k: data[k] for k in known_fields if k in data}
        extra = {k: v for k, v in data.items() if k not in ProofCreditOutcome.__dataclass_fields__}
        if extra:
            parsed["extra"] = extra
        return ProofCreditOutcome(**parsed)

    def _parse_historical_import(self, data: dict[str, Any]) -> HistoricalPaymentProofImport:
        return HistoricalPaymentProofImport(
            **{k: data[k] for k in HistoricalPaymentProofImport.__dataclass_fields__ if k in data}
        )

    # -- Auth --

    def get_me(self) -> Agent:
        """Get the authenticated agent's details."""
        data = self._request("GET", self._agent_path())
        return Agent(**{k: data[k] for k in Agent.__dataclass_fields__ if k in data})

    # -- Agent CRUD --

    def create_agent(self, name: str, **kwargs: Any) -> Agent:
        """Create a new agent."""
        payload: dict[str, Any] = {"name": name, **kwargs}
        data = self._request("POST", "/agents", json=payload)
        return Agent(**{k: data[k] for k in Agent.__dataclass_fields__ if k in data})

    def get_agent(self, agent_id: str) -> Agent:
        """Get an agent by ID."""
        data = self._request("GET", f"/agents/{agent_id}")
        return Agent(**{k: data[k] for k in Agent.__dataclass_fields__ if k in data})

    def list_agents(self, limit: int = 50, offset: int = 0) -> list[Agent]:
        """List agents for the authenticated operator."""
        params: dict[str, int] = {"limit": limit, "offset": offset}
        data = self._request("GET", "/agents", params=params)
        items = data.get("data", data) if isinstance(data, dict) else data
        return [
            Agent(**{k: item[k] for k in Agent.__dataclass_fields__ if k in item})
            for item in items
            if isinstance(item, dict)
        ]

    def approve_agent(
        self,
        agent_id: str,
        seed_amount: int = 0,
        seed_currency: str | None = None,
        seed_wallet_id: str | None = None,
    ) -> dict[str, Any]:
        """Approve an agent and optionally seed starter balance."""
        payload: dict[str, Any] = {}
        if seed_amount > 0:
            payload["seed_amount"] = seed_amount
        if seed_currency:
            payload["seed_currency"] = seed_currency
        if seed_wallet_id:
            payload["seed_wallet_id"] = seed_wallet_id
        return self._request("POST", f"/agents/{agent_id}/approve", json=payload)

    def batch_agents(self, items: list[dict[str, Any]]) -> dict[str, Any]:
        """Create multiple agents in a single request."""
        payload: dict[str, Any] = {"items": items}
        data = self._request("POST", "/agents/batch", json=payload)
        return data if isinstance(data, dict) else {}

    def suggest_agent_name(self) -> str:
        """Get a suggested agent name."""
        data = self._request("GET", "/agents/suggest-name")
        data_map = data if isinstance(data, dict) else {}
        return str(data_map.get("name", ""))

    # -- Historical proof imports --

    def import_payment_proof(
        self,
        proof_type: str,
        proof_ref: str,
        rail: str,
        amount: int,
        currency: str = "BTC",
        direction: str = "outbound",
        sender_address: str | None = None,
        recipient_address: str | None = None,
        occurred_at: str | None = None,
        counterparty_ref: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> HistoricalPaymentProofImport:
        """Import a historical on-chain payment proof for credit building.

        Both the sender (outbound) and recipient (inbound) can submit the same
        proof for dual-sided credit. When both counterparties submit, confidence
        is boosted to strong.

        Supported proof_type values:
        - btc_onchain_tx: Bitcoin on-chain transaction ID
        - eth_onchain_tx: Ethereum on-chain transaction hash
        - base_onchain_tx: Base on-chain transaction hash
        - sol_onchain_tx: Solana transaction signature

        Args:
            direction: "outbound" (you sent) or "inbound" (you received)
            sender_address: Required for outbound EVM proofs (must match a bound wallet)
            recipient_address: Required for inbound EVM proofs (must match a bound wallet)
            counterparty_ref: Optional identifier for the other party
        """
        payload: dict[str, Any] = {
            "proof_type": (proof_type or "").strip().lower(),
            "proof_ref": (proof_ref or "").strip(),
            "rail": (rail or "").strip().lower(),
            "currency": (currency or "BTC").strip().upper(),
            "amount": amount,
            "direction": (direction or "outbound").strip().lower(),
        }
        if sender_address:
            payload["sender_address"] = sender_address
        if recipient_address:
            payload["recipient_address"] = recipient_address
        if occurred_at:
            payload["occurred_at"] = occurred_at
        if counterparty_ref:
            payload["counterparty_ref"] = counterparty_ref
        if metadata is not None:
            payload["metadata"] = metadata
        data = self._request("POST", self._agent_path("/credit/imports/payment-proofs"), json=payload)
        imported = self._parse_historical_import(data.get("import", {}))
        imported.credit = self._parse_proof_credit(data.get("credit"))
        return imported

    def import_x402_receipt(
        self,
        tx: str,
        network: str,
        payer: str,
        payee: str,
        amount: str | int,
        asset: str | None = None,
        resource_url: str | None = None,
        scheme: str = "exact",
        direction: str = "outbound",
    ) -> dict[str, Any]:
        """Import an x402 settlement receipt for credit attribution.

        Both the payer (outbound) and payee (inbound) can submit the same
        receipt for dual-sided credit.

        Args:
            direction: "outbound" (you paid) or "inbound" (you received payment)
        """
        amount_str = str(amount)
        payload: dict[str, Any] = {
            "resource_url": resource_url or "",
            "direction": (direction or "outbound").strip().lower(),
            "requirements": {
                "scheme": scheme,
                "network": network,
                "amount": amount_str,
                "asset": asset or "",
                "payTo": payee,
                "maxTimeoutSeconds": 60,
            },
            "authorization": {
                "from": payer,
                "to": payee,
                "value": amount_str,
            },
            "settlement": {
                "success": True,
                "transaction": tx,
                "network": network,
                "payer": payer,
            },
        }
        data = self._request("POST", self._agent_path("/credit/imports/x402-receipts"), json=payload)
        return data if isinstance(data, dict) else {}

    def import_mpp_receipt(
        self,
        method: str,
        reference: str,
        challenge_id: str,
        challenge_method: str,
        challenge_intent: str,
        challenge_request: str,
        *,
        source: str = "",
        realm: str = "",
        resource_url: str = "",
        direction: str = "outbound",
    ) -> dict[str, Any]:
        """Import an MPP (Machine Payments Protocol) receipt.

        Supports Tempo (stablecoin), Lightning, Stripe, and Card payments.

        Args:
            method: Payment method — "tempo", "lightning", "stripe", or "card".
            reference: Transaction hash or payment reference.
            challenge_id: MPP challenge ID from the 402 response.
            challenge_method: Payment method from the challenge.
            challenge_intent: Payment intent from the challenge.
            challenge_request: Base64url-encoded challenge request JSON.
            source: Payer identifier (recommended: DID).
            realm: Server realm from the challenge.
            resource_url: The service URL that was paid for.
            direction: "outbound" (you paid) or "inbound" (you received).
        """
        from datetime import datetime, timezone

        payload: dict[str, Any] = {
            "receipt": {
                "status": "success",
                "method": method,
                "reference": reference,
                "timestamp": datetime.now(timezone.utc).isoformat(),
            },
            "credential": {
                "challenge": {
                    "id": challenge_id,
                    "method": challenge_method,
                    "intent": challenge_intent,
                    "request": challenge_request,
                    "realm": realm,
                },
                "source": source,
            },
            "resource_url": resource_url,
            "direction": (direction or "outbound").strip().lower(),
        }
        data = self._request("POST", self._agent_path("/credit/imports/mpp-receipts"), json=payload)
        return data if isinstance(data, dict) else {}

    def list_payment_proof_imports(
        self,
        limit: int = 50,
        offset: int = 0,
    ) -> list[HistoricalPaymentProofImport]:
        """List historical BTC payment proof imports for this agent."""
        params: dict[str, int] = {"limit": limit, "offset": offset}
        data = self._request("GET", self._agent_path("/credit/imports/payment-proofs"), params=params)
        items = data.get("data", data) if isinstance(data, dict) else data
        return [
            self._parse_historical_import(item)
            for item in items
            if isinstance(item, dict)
        ]

    def reverify_payment_proof_import(self, import_id: str) -> HistoricalPaymentProofImport:
        """Re-run verification on a historical payment proof import."""
        data = self._request(
            "POST",
            self._agent_path(f"/credit/imports/payment-proofs/{import_id}/reverify"),
            json={},
        )
        imported = self._parse_historical_import(data.get("import", {}))
        imported.credit = self._parse_proof_credit(data.get("credit"))
        return imported

    # -- Agent credit --

    def get_agent_credit(self) -> AgentCreditResponse:
        """
        .. deprecated::
            The /agents/{id}/credit endpoint has been removed.
            Use :meth:`get_score` instead to get BOB Score and tier.
        """
        raise RuntimeError(
            "get_agent_credit() is removed — the /agents/{id}/credit endpoint no longer exists. "
            "Use get_score() instead."
        )

    def list_agent_credit_events(self, limit: int = 50, offset: int = 0) -> list[CreditEvent]:
        """Fetch paginated credit event history for the agent."""
        data = self._request(
            "GET",
            self._agent_path("/credit/events"),
            params={"limit": limit, "offset": offset},
        )
        items = data.get("data", []) if isinstance(data, dict) else []
        return [
            CreditEvent(**{k: item[k] for k in CreditEvent.__dataclass_fields__ if k in item})
            for item in items
        ]

    # -- BOB Score --

    def get_score(self) -> dict[str, Any]:
        """Get the authenticated operator's BOB Score."""
        data = self._request("GET", "/score/me")
        return data if isinstance(data, dict) else {}

    def get_score_composition(self) -> dict[str, Any]:
        """Get the detailed BOB Score composition breakdown for the operator."""
        data = self._request("GET", "/score/me/composition")
        return data if isinstance(data, dict) else {}

    def update_signal_visibility(
        self,
        signal_type: str,
        is_public: bool,
    ) -> dict[str, Any]:
        """Update whether a trust signal is publicly visible."""
        data = self._request(
            "PATCH",
            "/score/me/signals/visibility",
            json={"signal_type": signal_type, "is_public": is_public},
        )
        return data if isinstance(data, dict) else {}

    def get_leaderboard(
        self,
        limit: int = 20,
    ) -> dict[str, Any]:
        """Get the global BOB Score leaderboard."""
        params: dict[str, int] = {"limit": limit}
        data = self._request("GET", "/score/leaderboard", params=params)
        return data if isinstance(data, dict) else {}

    # -- Wallet binding --

    def create_wallet_binding_challenge(
        self,
        address: str,
        rail: str = "evm",
    ) -> dict[str, Any]:
        """Create a challenge for binding an operator wallet address."""
        payload: dict[str, Any] = {"address": (address or "").strip()}
        data = self._request("POST", f"/operators/me/wallet-bindings/{rail.strip().lower()}/challenge", json=payload)
        return data if isinstance(data, dict) else {}

    def verify_wallet_binding(
        self,
        challenge_id: str,
        address: str,
        signature: str,
        rail: str = "evm",
        chain_id: str | None = None,
        wallet_type: str | None = None,
    ) -> dict[str, Any]:
        """Verify an operator wallet binding by submitting a signed challenge."""
        payload: dict[str, Any] = {
            "challenge_id": (challenge_id or "").strip(),
            "address": (address or "").strip(),
            "signature": (signature or "").strip(),
        }
        if chain_id:
            payload["chain_id"] = chain_id
        if wallet_type:
            payload["wallet_type"] = wallet_type
        data = self._request("POST", f"/operators/me/wallet-bindings/{rail.strip().lower()}/verify", json=payload)
        return data if isinstance(data, dict) else {}

    # -- Credentials --

    def issue_credential(self) -> dict[str, Any]:
        """Issue a new BOB credential for the authenticated operator."""
        data = self._request("POST", "/operators/me/credential")
        return data if isinstance(data, dict) else {}

    def get_credential(self) -> dict[str, Any]:
        """Fetch the latest active BOB credential for the authenticated operator."""
        data = self._request("GET", "/operators/me/credential")
        return data if isinstance(data, dict) else {}

    def verify_credential(self, credential_id: str) -> dict[str, Any]:
        """Verify a BOB credential by ID."""
        data = self._request("GET", f"/credentials/{credential_id}")
        return data if isinstance(data, dict) else {}

    def get_passport_jwt(self) -> str | None:
        """Get the JWT bearer token for this agent's passport.

        The agent uses this in outbound requests:
            Authorization: Bearer <jwt>

        Returns None if no passport exists or JWT is not available.
        """
        try:
            data = self._request("POST", self._agent_path("/credential"))
            if isinstance(data, dict) and data.get("jwt"):
                return data["jwt"]
        except Exception:
            pass
        try:
            data = self._request("GET", self._agent_path("/credential"))
            if isinstance(data, dict) and data.get("jwt"):
                return data["jwt"]
        except Exception:
            pass
        return None

    # -- Social --

    def connect_social(
        self,
        platform: str,
        access_token: str,
        agent_id: str | None = None,
    ) -> dict[str, Any]:
        """Connect a social account to an agent for BOB Score signals."""
        target = agent_id or self.agent_id
        if not target:
            raise ValueError("agent_id is required")
        payload: dict[str, Any] = {
            "platform": (platform or "").strip().lower(),
            "access_token": (access_token or "").strip(),
        }
        data = self._request("POST", f"/agents/{target}/social/connect", json=payload)
        return data if isinstance(data, dict) else {}

    # -- Webhooks --

    def create_agent_webhook(self, url: str, events: list[str] | None = None) -> WebhookSubscriber:
        """Create an agent-scoped webhook subscriber."""
        payload: dict[str, Any] = {"url": (url or "").strip()}
        if events is not None:
            payload["events"] = [str(event).strip() for event in events if str(event).strip()]
        data = self._request("POST", self._agent_path("/webhooks"), json=payload)
        data_map = data if isinstance(data, dict) else {}
        return WebhookSubscriber(
            **{k: data_map[k] for k in WebhookSubscriber.__dataclass_fields__ if k in data_map}
        )

    def list_agent_webhooks(self) -> list[WebhookSubscriber]:
        """List webhook subscribers scoped to this agent."""
        data = self._request("GET", self._agent_path("/webhooks"))
        items = data if isinstance(data, list) else []
        return [
            WebhookSubscriber(
                **{k: item[k] for k in WebhookSubscriber.__dataclass_fields__ if k in item}
            )
            for item in items
            if isinstance(item, dict)
        ]

    def get_agent_webhook(self, webhook_id: str) -> WebhookSubscriber:
        """Fetch one agent webhook subscriber by ID."""
        data = self._request("GET", self._agent_path(f"/webhooks/{webhook_id}"))
        data_map = data if isinstance(data, dict) else {}
        return WebhookSubscriber(
            **{k: data_map[k] for k in WebhookSubscriber.__dataclass_fields__ if k in data_map}
        )

    def update_agent_webhook(
        self,
        webhook_id: str,
        *,
        url: str | None = None,
        events: list[str] | None = None,
        active: bool | None = None,
    ) -> WebhookSubscriber:
        """Update an agent-scoped webhook subscriber."""
        payload: dict[str, Any] = {}
        if url is not None:
            payload["url"] = url.strip()
        if events is not None:
            payload["events"] = [str(event).strip() for event in events if str(event).strip()]
        if active is not None:
            payload["active"] = bool(active)
        data = self._request("PATCH", self._agent_path(f"/webhooks/{webhook_id}"), json=payload)
        data_map = data if isinstance(data, dict) else {}
        return WebhookSubscriber(
            **{k: data_map[k] for k in WebhookSubscriber.__dataclass_fields__ if k in data_map}
        )

    def delete_agent_webhook(self, webhook_id: str) -> dict[str, Any]:
        """Delete an agent-scoped webhook subscriber."""
        data = self._request("DELETE", self._agent_path(f"/webhooks/{webhook_id}"))
        return data if isinstance(data, dict) else {}

    # -- Inbox --

    def get_agent_inbox(
        self,
        status: str | None = None,
        limit: int = 50,
        offset: int = 0,
    ) -> list[InboxEvent]:
        """List inbox events for this agent."""
        params: dict[str, str | int] = {"limit": limit, "offset": offset}
        if status:
            params["status"] = status
        data = self._request("GET", self._agent_path("/inbox"), params=params)
        items = data.get("data", data) if isinstance(data, dict) else data
        return [
            InboxEvent(**{k: item[k] for k in InboxEvent.__dataclass_fields__ if k in item})
            for item in items
            if isinstance(item, dict)
        ]

    def ack_inbox_event(self, event_id: str) -> InboxEvent:
        """Acknowledge an inbox event."""
        data = self._request("POST", self._agent_path(f"/inbox/{event_id}/ack"))
        return InboxEvent(**{k: data[k] for k in InboxEvent.__dataclass_fields__ if k in data})

    # -- Public directory & messaging --

    def search_directory(
        self,
        query: str | None = None,
        min_score: int = 0,
        tier: str | None = None,
        limit: int = 20,
        offset: int = 0,
    ) -> list[DirectoryEntry]:
        """Search publicly discoverable agents by handle/score/tier."""
        params: dict[str, Any] = {"limit": limit, "offset": offset}
        if query:
            params["q"] = query.strip()
        if min_score > 0:
            params["min_score"] = min_score
        if tier:
            params["tier"] = tier.strip()
        data = self._request("GET", "/public/directory", params=params)
        rows = data.get("agents", []) if isinstance(data, dict) else []
        return [
            DirectoryEntry(**{k: row[k] for k in DirectoryEntry.__dataclass_fields__ if k in row})
            for row in rows
            if isinstance(row, dict)
        ]

    def get_public_card(self, handle: str) -> dict[str, Any]:
        """Get the public card for an agent handle."""
        normalized = (handle or "").strip().lstrip("@")
        if not normalized:
            raise ValueError("handle is required")
        data = self._request("GET", f"/public/handle/{normalized}/card")
        return data if isinstance(data, dict) else {}

    def get_public_ledger(self, handle: str, limit: int = 30, offset: int = 0) -> dict[str, Any]:
        """Get the public transaction ledger for an agent handle."""
        normalized = (handle or "").strip().lstrip("@")
        if not normalized:
            raise ValueError("handle is required")
        data = self._request("GET", f"/public/handle/{normalized}/ledger", params={"limit": limit, "offset": offset})
        return data if isinstance(data, dict) else {}

    def send_message(
        self,
        recipient_handle: str,
        body: str,
        is_public: bool = False,
        proof_import_id: str | None = None,
        agent_id: str | None = None,
    ) -> dict[str, Any]:
        """Send a message from this agent to another agent by handle."""
        aid = (agent_id or self.agent_id or "").strip()
        if not aid:
            raise ValueError("agent_id is required")
        target = (recipient_handle or "").strip().lstrip("@")
        if not target:
            raise ValueError("recipient_handle is required")
        payload: dict[str, Any] = {
            "recipient_handle": target,
            "body": body,
            "is_public": bool(is_public),
        }
        if proof_import_id:
            payload["proof_import_id"] = proof_import_id.strip()
        data = self._request("POST", f"/agents/{aid}/messages", json=payload)
        return data if isinstance(data, dict) else {}

    def list_public_messages(
        self,
        handle: str,
        limit: int = 30,
        offset: int = 0,
    ) -> list[PublicAgentMessage]:
        """List public messages for a specific handle."""
        normalized = (handle or "").strip().lstrip("@")
        if not normalized:
            raise ValueError("handle is required")
        data = self._request(
            "GET",
            f"/public/handle/{normalized}/messages",
            params={"limit": limit, "offset": offset},
        )
        rows = data.get("data", []) if isinstance(data, dict) else []
        return [
            PublicAgentMessage(**{k: row[k] for k in PublicAgentMessage.__dataclass_fields__ if k in row})
            for row in rows
            if isinstance(row, dict)
        ]

    def get_public_message_feed(self, limit: int = 30, offset: int = 0) -> list[PublicAgentMessage]:
        """List network-wide public messages."""
        data = self._request("GET", "/public/feed", params={"limit": limit, "offset": offset})
        rows = data.get("data", []) if isinstance(data, dict) else []
        return [
            PublicAgentMessage(**{k: row[k] for k in PublicAgentMessage.__dataclass_fields__ if k in row})
            for row in rows
            if isinstance(row, dict)
        ]

    def list_agent_events(self, limit: int = 30, offset: int = 0) -> list[dict[str, Any]]:
        """List agent system events (paginated)."""
        data = self._request(
            "GET",
            self._agent_path("/events"),
            params={"limit": limit, "offset": offset},
        )
        items = data.get("data", []) if isinstance(data, dict) else []
        return [item for item in items if isinstance(item, dict)]

    # -- API keys --

    def list_api_keys(
        self,
        include_revoked: bool = False,
        limit: int = 50,
    ) -> list[APIKeyRecord]:
        """List API keys for the authenticated operator."""
        if limit <= 0:
            raise ValueError("limit must be > 0")
        params: dict[str, Any] = {"limit": limit}
        if include_revoked:
            params["include_revoked"] = "true"
        data = self._request("GET", "/operators/me/api-keys", params=params)
        items = data if isinstance(data, list) else []
        return [
            APIKeyRecord(**{k: item[k] for k in APIKeyRecord.__dataclass_fields__ if k in item})
            for item in items
            if isinstance(item, dict)
        ]

    def create_api_key(
        self,
        name: str = "",
        scopes: list[str] | None = None,
    ) -> dict[str, Any]:
        """Create a new API key with optional scopes.

        Returns a dict with ``api_key`` (the raw secret, shown once) and
        ``key`` (an ``APIKeyRecord``-shaped dict).
        """
        payload: dict[str, Any] = {}
        if (name or "").strip():
            payload["name"] = name.strip()
        if scopes:
            payload["scopes"] = [str(scope).strip().lower() for scope in scopes if str(scope).strip()]
        data = self._request("POST", "/operators/me/api-keys", json=payload)
        return data if isinstance(data, dict) else {}

    def revoke_api_key(self, key_id: str) -> dict[str, Any]:
        """Revoke an API key by ID."""
        normalized = (key_id or "").strip()
        if not normalized:
            raise ValueError("key_id is required")
        data = self._request("POST", f"/operators/me/api-keys/{normalized}/revoke", json={})
        return data if isinstance(data, dict) else {}

    # ------------------------------------------------------------------
    # Wallets & Treasury
    # ------------------------------------------------------------------

    def register_wallet(
        self,
        rail: str,
        address: str,
        key_fingerprint: str = "",
        agent_id: str | None = None,
    ) -> AgentWallet:
        """Register a non-custodial wallet address for an agent."""
        aid = (agent_id or self.agent_id or "").strip()
        if not aid:
            raise ValueError("agent_id is required")
        payload: dict[str, Any] = {"rail": rail.strip().lower(), "address": address.strip()}
        if key_fingerprint:
            payload["key_fingerprint"] = key_fingerprint.strip()
        data = self._request("POST", f"/agents/{aid}/wallets", json=payload)
        return AgentWallet(**{k: v for k, v in data.items() if k in AgentWallet.__dataclass_fields__})

    def list_wallets(self, agent_id: str | None = None) -> list[AgentWallet]:
        """List registered wallets for an agent."""
        aid = (agent_id or self.agent_id or "").strip()
        if not aid:
            raise ValueError("agent_id is required")
        data = self._request("GET", f"/agents/{aid}/wallets")
        if not isinstance(data, list):
            return []
        return [
            AgentWallet(**{k: v for k, v in w.items() if k in AgentWallet.__dataclass_fields__})
            for w in data
        ]

    def get_balance(self, agent_id: str | None = None) -> list[CurrencyBalance]:
        """Get proven balance from verified payment proofs."""
        aid = (agent_id or self.agent_id or "").strip()
        if not aid:
            raise ValueError("agent_id is required")
        data = self._request("GET", f"/agents/{aid}/balance")
        balances = data.get("balances", []) if isinstance(data, dict) else []
        return [
            CurrencyBalance(**{k: v for k, v in b.items() if k in CurrencyBalance.__dataclass_fields__})
            for b in balances
        ]

    def get_credit_limit(self, agent_id: str | None = None) -> CreditLimitResponse:
        """Get computed credit limit (score x balance x age)."""
        aid = (agent_id or self.agent_id or "").strip()
        if not aid:
            raise ValueError("agent_id is required")
        data = self._request("GET", f"/agents/{aid}/credit-limit")
        limits = [
            CurrencyLimit(**{k: v for k, v in lim.items() if k in CurrencyLimit.__dataclass_fields__})
            for lim in data.get("limits", [])
        ]
        return CreditLimitResponse(
            limits=limits,
            score=data.get("score", 0),
            tier=data.get("tier", ""),
            agent_age_days=data.get("agent_age_days", 0),
        )

    # ------------------------------------------------------------------
    # Loans
    # ------------------------------------------------------------------

    def create_loan_offer(
        self,
        safe_address: str,
        max_amount: int,
        interest_rate_bps: int,
        min_borrower_score: int = 400,
        min_borrower_tier: str = "Verified",
        duration_days: int = 90,
        grace_period_days: int = 3,
        agent_id: str | None = None,
    ) -> dict[str, Any]:
        """Create a loan offer (credit line) backed by a Gnosis Safe."""
        aid = (agent_id or self.agent_id or "").strip()
        if not aid:
            raise ValueError("agent_id is required")
        data = self._request("POST", "/loans/offers", json={
            "agent_id": aid,
            "safe_address": safe_address.strip(),
            "max_amount": max_amount,
            "interest_rate_bps": interest_rate_bps,
            "min_borrower_score": min_borrower_score,
            "min_borrower_tier": min_borrower_tier,
            "duration_days": duration_days,
            "grace_period_days": grace_period_days,
        })
        return data if isinstance(data, dict) else {}

    def list_loan_offers(
        self, agent_id: str | None = None, limit: int = 30, offset: int = 0,
    ) -> list[dict[str, Any]]:
        """List loan offers created by the agent."""
        aid = (agent_id or self.agent_id or "").strip()
        data = self._request("GET", "/loans/offers", params={
            "agent_id": aid, "limit": limit, "offset": offset,
        })
        return data if isinstance(data, list) else []

    def browse_marketplace(
        self, limit: int = 30, offset: int = 0,
    ) -> list[dict[str, Any]]:
        """Browse all active loan offers."""
        data = self._request("GET", "/loans/marketplace", params={
            "limit": limit, "offset": offset,
        })
        return data if isinstance(data, list) else []

    def accept_loan(
        self,
        offer_id: str,
        amount: int,
        borrower_wallet_address: str = "",
        agent_id: str | None = None,
    ) -> dict[str, Any]:
        """Accept a loan offer."""
        aid = (agent_id or self.agent_id or "").strip()
        if not aid:
            raise ValueError("agent_id is required")
        data = self._request("POST", f"/loans/offers/{offer_id}/accept", json={
            "amount": amount,
            "borrower_wallet_address": borrower_wallet_address,
            "agent_id": aid,
        })
        return data if isinstance(data, dict) else {}

    def record_draw(
        self, loan_id: str, tx_hash: str, agent_id: str | None = None,
    ) -> dict[str, Any]:
        """Record a loan draw (submit funding tx hash)."""
        aid = (agent_id or self.agent_id or "").strip()
        data = self._request("POST", f"/loans/agreements/{loan_id}/draws", json={
            "tx_hash": tx_hash, "agent_id": aid,
        })
        return data if isinstance(data, dict) else {}

    def submit_repayment(
        self, loan_id: str, tx_hash: str, amount: int, agent_id: str | None = None,
    ) -> dict[str, Any]:
        """Submit a loan repayment proof."""
        aid = (agent_id or self.agent_id or "").strip()
        data = self._request("POST", f"/loans/agreements/{loan_id}/repayments", json={
            "tx_hash": tx_hash, "amount": amount, "agent_id": aid,
        })
        return data if isinstance(data, dict) else {}

    def list_loans(
        self, agent_id: str | None = None, limit: int = 30, offset: int = 0,
    ) -> list[dict[str, Any]]:
        """List loan agreements."""
        aid = (agent_id or self.agent_id or "").strip()
        data = self._request("GET", "/loans/agreements", params={
            "agent_id": aid, "limit": limit, "offset": offset,
        })
        return data if isinstance(data, list) else []

    def get_loan_status(self, loan_id: str) -> dict[str, Any]:
        """Get loan agreement details."""
        data = self._request("GET", f"/loans/agreements/{loan_id}")
        return data if isinstance(data, dict) else {}

    def get_lender_status(self) -> dict[str, Any]:
        """Check whether the authenticated operator is approved as a lender."""
        data = self._request("GET", "/loans/lender-status")
        return data if isinstance(data, dict) else {}

    def get_loan_eligibility(self) -> "LoanEligibility":
        """Check the authenticated operator's loan eligibility (borrower side)."""
        from .types import LoanEligibility

        data = self._request("GET", "/loans/eligibility")
        data_map = data if isinstance(data, dict) else {}
        return LoanEligibility(
            **{k: data_map[k] for k in LoanEligibility.__dataclass_fields__ if k in data_map}
        )
