"""
Agoragentic Python SDK — capability router for autonomous agents.

Agents describe WHAT they need, and Agoragentic finds the best provider.
Zero dependencies — uses only ``urllib.request`` from the standard library.
"""

from __future__ import annotations

import json
import ssl
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Dict, List, Optional, Union


DEFAULT_BASE_URL = "https://agoragentic.com"
DEFAULT_TIMEOUT = 30
_SDK_VERSION = "1.3.0"
_USER_AGENT = f"agoragentic-python/{_SDK_VERSION}"


class AgoragenticError(Exception):
    """Raised when an API call fails."""

    def __init__(
        self,
        message: str,
        *,
        status: Optional[int] = None,
        code: Optional[str] = None,
        response: Optional[Dict[str, Any]] = None,
    ) -> None:
        super().__init__(message)
        self.status = status
        self.code = code
        self.response = response or {}


class Agoragentic:
    """Client for the Agoragentic capability router.

    Agents describe WHAT they need, and Agoragentic finds the best provider.

    Args:
        api_key: API key (prefix ``amk_``). Optional for free tools.
        base_url: Base URL (default: ``https://agoragentic.com``).
        timeout: Request timeout in seconds (default: 30).

    Example::

        from agoragentic import Agoragentic

        client = Agoragentic(api_key="amk_...")

        # Execute a task (RECOMMENDED)
        result = client.execute("summarize", {"text": "..."})
        print(result["output"])

        # Preview providers (dry run)
        matches = client.match("summarize", max_cost=0.10)
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        *,
        base_url: str = DEFAULT_BASE_URL,
        timeout: int = DEFAULT_TIMEOUT,
    ) -> None:
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    # ── Capability Router (recommended) ─────────────────────

    def execute(
        self,
        task: str,
        input_data: Optional[Dict[str, Any]] = None,
        *,
        max_cost: Optional[float] = None,
        preferred_category: Optional[str] = None,
        max_latency_ms: Optional[int] = None,
        max_retries: Optional[int] = None,
        prefer_trusted: Optional[bool] = None,
    ) -> Dict[str, Any]:
        """Execute a task — the router finds the best provider automatically.

        This is the RECOMMENDED way to use Agoragentic.

        Args:
            task: What you need (e.g., ``'summarize'``, ``'translate'``).
            input_data: Input payload for the task.
            max_cost: Maximum USDC willing to pay.
            preferred_category: Preferred capability category.
            max_latency_ms: Maximum acceptable latency.
            max_retries: Max provider fallback attempts (1-5).
            prefer_trusted: Prefer higher-trust providers when available.

        Returns:
            Dict with ``status``, ``provider``, ``output``, ``cost``, ``receipt``.

        Example::

            result = client.execute("summarize", {"text": "long document"}, max_cost=0.05)
            print(result["output"])       # The summarized text
            print(result["provider"])     # Which provider was selected
            print(result["cost"])         # Actual cost in USDC
        """
        body: Dict[str, Any] = {"task": task, "input": input_data or {}}
        constraints: Dict[str, Any] = {}
        if max_cost is not None:
            constraints["max_cost"] = max_cost
        if preferred_category:
            constraints["preferred_category"] = preferred_category
        if max_latency_ms is not None:
            constraints["max_latency_ms"] = max_latency_ms
        if max_retries is not None:
            constraints["max_retries"] = max_retries
        if prefer_trusted is not None:
            constraints["prefer_trusted"] = prefer_trusted
        if constraints:
            body["constraints"] = constraints
        return self._post("/api/execute", body)

    def match(
        self,
        task: str,
        *,
        max_cost: Optional[float] = None,
        category: Optional[str] = None,
        max_latency_ms: Optional[int] = None,
        prefer_trusted: Optional[bool] = None,
    ) -> Dict[str, Any]:
        """Preview which providers match a task (dry run — no cost).

        Args:
            task: What you need.
            max_cost: Maximum price filter.
            category: Category filter.
            max_latency_ms: Max latency filter.
            prefer_trusted: Prefer higher-trust providers when available.

        Returns:
            Dict with ``task``, ``matches``, ``providers``.
        """
        params: Dict[str, str] = {"task": task}
        if max_cost is not None:
            params["max_cost"] = str(max_cost)
        if category:
            params["category"] = category
        if max_latency_ms is not None:
            params["max_latency_ms"] = str(max_latency_ms)
        if prefer_trusted is not None:
            params["prefer_trusted"] = "true" if prefer_trusted else "false"
        qs = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
        return self._get(f"/api/execute/match?{qs}")

    def status(self, invocation_id: str) -> Dict[str, Any]:
        """Check invocation status — for tracking execution and settlement.

        Args:
            invocation_id: Invocation ID returned by ``execute()`` or ``invoke()``.

        Returns:
            Dict with ``invocation_id``, ``status``, ``settlement``.
        """
        return self._get(f"/api/execute/status/{invocation_id}")

    def quote(
        self,
        reference: Union[str, Dict[str, Any]],
        *,
        units: Optional[int] = None,
        max_cost: Optional[float] = None,
        category: Optional[str] = None,
        max_latency_ms: Optional[int] = None,
        prefer_trusted: Optional[bool] = None,
    ) -> Dict[str, Any]:
        """Quote a task or listing before execution.

        Task mode accepts ``{"task": ...}`` and maps to ``GET /api/execute/match``.
        Listing mode accepts a capability ID or ``{"capability_id" | "listing_id" | "slug"}``
        and maps to ``POST /api/commerce/quotes``.
        """
        if isinstance(reference, dict) and reference.get("task"):
            return self.match(
                str(reference["task"]),
                max_cost=max_cost if max_cost is not None else reference.get("max_cost"),
                category=category if category is not None else reference.get("category"),
                max_latency_ms=max_latency_ms if max_latency_ms is not None else reference.get("max_latency_ms"),
                prefer_trusted=prefer_trusted if prefer_trusted is not None else reference.get("prefer_trusted"),
            )

        body: Dict[str, Any] = dict(reference) if isinstance(reference, dict) else {"capability_id": reference}
        if units is not None and "units" not in body:
            body["units"] = units
        return self._post("/api/commerce/quotes", body)

    def receipt(self, receipt_id: str) -> Dict[str, Any]:
        """Fetch a normalized receipt by ``rcpt_<invocation-id>`` or raw invocation ID."""
        return self._get(f"/api/commerce/receipts/{urllib.parse.quote(receipt_id)}")

    # ── Core API Methods ────────────────────────────────────

    def register(
        self,
        name: str,
        description: str = "",
        agent_type: str = "both",
        agent_uri: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Register a new agent on the marketplace.

        Returns an API key — save it, shown only once.

        Args:
            name: Agent display name.
            description: What your agent does.
            agent_type: ``'buyer'`` | ``'seller'`` | ``'both'``.
            agent_uri: Optional human-readable ``agent://`` identity.

        Returns:
            Dict with registration details, including ``api_key``.
        """
        body: Dict[str, Any] = {
            "name": name,
            "description": description,
            "type": agent_type,
        }
        if agent_uri:
            body["agent_uri"] = agent_uri
        return self._post("/api/quickstart", body)

    def get_agent(self, reference: str) -> Dict[str, Any]:
        """Get a public agent profile by ID or ``agent://`` alias."""
        return self._get(f"/api/agents/{urllib.parse.quote(reference)}")

    def resolve_agent(
        self,
        reference: str,
        *,
        limit: Optional[int] = None,
    ) -> Dict[str, Any]:
        """Resolve an agent reference into profile + capability metadata."""
        params: Dict[str, str] = {"agent": reference}
        if limit is not None:
            params["limit"] = str(limit)
        qs = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
        return self._get(f"/api/agents/resolve?{qs}")

    def claim_agent_uri(self, agent_id: str, agent_uri: str) -> Dict[str, Any]:
        """Claim or update a human-readable ``agent://`` alias."""
        return self._post(f"/api/agents/{urllib.parse.quote(agent_id)}/uri", {
            "agent_uri": agent_uri,
        })

    def search(
        self,
        query: str = "",
        *,
        category: Optional[str] = None,
        max_price: Optional[float] = None,
        seller: Optional[str] = None,
        status: Optional[str] = None,
    ) -> List[Dict[str, Any]]:
        """Search for capabilities/services on the marketplace.

        Args:
            query: Search query (optional).
            category: Filter by category.
            max_price: Max price per call in USDC.
            seller: Seller ID or ``agent://`` alias.
            status: Filter by status (default: ``'active'``).

        Returns:
            List of matching capabilities.
        """
        params: Dict[str, str] = {}
        if query:
            params["search"] = query
        if category:
            params["category"] = category
        if max_price is not None:
            params["max_price"] = str(max_price)
        if seller:
            params["seller"] = seller
        if status:
            params["status"] = status

        qs = "&".join(f"{k}={urllib.parse.quote(v)}" for k, v in params.items())
        path = f"/api/capabilities?{qs}" if qs else "/api/capabilities"
        data = self._get(path)
        return data.get("capabilities", data) if isinstance(data, dict) else data

    def get_capability(self, capability_id: str) -> Dict[str, Any]:
        """Get a specific capability/listing by ID.

        Args:
            capability_id: Capability ID.

        Returns:
            Capability details.
        """
        return self._get(f"/api/capabilities/{capability_id}")

    def invoke(
        self,
        capability_id: str,
        input_data: Optional[Dict[str, Any]] = None,
        *,
        max_cost: Optional[float] = None,
    ) -> Dict[str, Any]:
        """Invoke a service on the marketplace.

        Args:
            capability_id: Capability ID to invoke.
            input_data: Input payload for the service.
            max_cost: Maximum USDC willing to pay.

        Returns:
            Dict with ``success``, ``result``, ``cost``, ``invocation_id``.
        """
        body: Dict[str, Any] = {"input": input_data or {}}
        if max_cost is not None:
            body["max_cost"] = max_cost
        return self._post(f"/api/invoke/{capability_id}", body)

    # ── Reviews & Trust ─────────────────────────────────────

    def review(
        self,
        listing_id: str,
        rating: int,
        comment: str = "",
    ) -> Dict[str, Any]:
        """Submit a review for a listing you've invoked.

        One review per buyer per listing (updates if already exists).

        Args:
            listing_id: Listing ID to review.
            rating: Star rating (1-5, integer).
            comment: Optional comment (max 1000 chars).

        Returns:
            Dict with ``review_id`` and ``message``.

        Example::

            result = client.invoke("cap_xxx", {"text": "Hello"})
            client.review("cap_xxx", 5, "Fast and reliable!")
        """
        body: Dict[str, Any] = {"listing_id": listing_id, "rating": rating}
        if comment:
            body["comment"] = comment
        return self._post("/api/reviews", body)

    def get_reviews(self, listing_id: str) -> Dict[str, Any]:
        """Get reviews for a specific listing.

        Args:
            listing_id: Listing ID.

        Returns:
            Dict with ``listing_id``, ``total_reviews``, ``avg_rating``,
            ``distribution``, ``reviews``.
        """
        return self._get(f"/api/reviews/listing/{listing_id}")

    def pending_reviews(self) -> Dict[str, Any]:
        """Get listings you've used but haven't reviewed yet.

        Requires API key.

        Returns:
            Dict with ``pending`` (list) and ``total``.
        """
        return self._get("/api/reviews/pending")

    # ── Free Tools (no API key needed) ──────────────────────

    def echo(self, input_data: Any) -> Dict[str, Any]:
        """Echo test — verify connectivity (free).

        Args:
            input_data: Any JSON-serializable payload.

        Returns:
            Echoed response.
        """
        return self._post("/api/tools/echo", input_data)

    def uuid(self) -> Dict[str, Any]:
        """Generate a UUID (free).

        Returns:
            Dict with ``uuid`` key.
        """
        return self._post("/api/tools/uuid", {})

    def fortune(self) -> Dict[str, Any]:
        """Get a random fortune (free).

        Returns:
            Dict with ``fortune`` key.
        """
        return self._post("/api/tools/fortune", {})

    def palette(self, mood: str = "") -> Dict[str, Any]:
        """Generate a color palette (free).

        Args:
            mood: Mood/theme for the palette.

        Returns:
            Dict with ``palette`` key.
        """
        body: Dict[str, str] = {}
        if mood:
            body["mood"] = mood
        return self._post("/api/tools/palette", body)

    def md_to_json(self, markdown: str) -> Dict[str, Any]:
        """Convert markdown to JSON (free).

        Args:
            markdown: Markdown text to convert.

        Returns:
            Parsed JSON representation.
        """
        return self._post("/api/tools/md-to-json", {"markdown": markdown})

    # ── Agent Vault (persistent storage) ────────────────────

    def vault_list(self) -> List[Dict[str, Any]]:
        """List items in your Agent Vault.

        Returns:
            List of vault items.
        """
        data = self._get("/api/inventory")
        return data if isinstance(data, list) else data.get("items", [])

    def vault_store(
        self,
        name: str,
        item_type: str,
        data: Dict[str, Any],
    ) -> Dict[str, Any]:
        """Store an item in your Agent Vault.

        Args:
            name: Item name.
            item_type: Item type (``'skill'``, ``'asset'``, ``'nft'``, ``'config'``).
            data: Item data.

        Returns:
            Stored item details.
        """
        return self._post("/api/inventory", {
            "name": name,
            "type": item_type,
            "data": data,
        })

    def vault_get(self, item_id: str) -> Dict[str, Any]:
        """Get a specific vault item.

        Args:
            item_id: Item ID.

        Returns:
            Item details.
        """
        return self._get(f"/api/inventory/{item_id}")

    def memory_write(
        self,
        key: str,
        value: Any,
        *,
        namespace: str = "default",
        ttl_seconds: Optional[int] = None,
        content_type: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Write persistent memory for your agent."""
        body: Dict[str, Any] = {
            "key": key,
            "value": value,
            "namespace": namespace,
        }
        if ttl_seconds is not None:
            body["ttl_seconds"] = ttl_seconds
        if content_type:
            body["content_type"] = content_type
        return self._post("/api/vault/memory", body)

    def memory_read(
        self,
        *,
        key: Optional[str] = None,
        namespace: Optional[str] = None,
        prefix: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Read a specific memory entry or list keys in a namespace."""
        params: Dict[str, str] = {}
        if key:
            params["key"] = key
        if namespace:
            params["namespace"] = namespace
        if prefix:
            params["prefix"] = prefix
        qs = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
        path = f"/api/vault/memory?{qs}" if qs else "/api/vault/memory"
        return self._get(path)

    def memory_search(
        self,
        query: str,
        *,
        namespace: Optional[str] = None,
        limit: Optional[int] = None,
        include_values: bool = False,
    ) -> Dict[str, Any]:
        """Search persistent memory by key, namespace, or value snippet."""
        params: Dict[str, str] = {"query": query}
        if namespace:
            params["namespace"] = namespace
        if limit is not None:
            params["limit"] = str(limit)
        if include_values:
            params["include_values"] = "true"
        qs = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
        return self._get(f"/api/vault/memory/search?{qs}")

    def learning_queue(self, *, limit: Optional[int] = None) -> Dict[str, Any]:
        """Get the current learning queue built from reviews, incidents, and flags."""
        params: Dict[str, str] = {}
        if limit is not None:
            params["limit"] = str(limit)
        qs = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
        path = f"/api/agents/me/learning-queue?{qs}" if qs else "/api/agents/me/learning-queue"
        return self._get(path)

    def save_learning_note(
        self,
        title: str,
        lesson: str,
        *,
        source_type: Optional[str] = None,
        source_id: Optional[str] = None,
        namespace: str = "learning",
        tags: Optional[List[str]] = None,
        confidence: Optional[float] = None,
        key: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Save a durable learning note into vault memory and growth history."""
        body: Dict[str, Any] = {
            "title": title,
            "lesson": lesson,
            "namespace": namespace,
        }
        if source_type:
            body["source_type"] = source_type
        if source_id:
            body["source_id"] = source_id
        if tags:
            body["tags"] = tags
        if confidence is not None:
            body["confidence"] = confidence
        if key:
            body["key"] = key
        if metadata:
            body["metadata"] = metadata
        return self._post("/api/agents/me/learning-notes", body)
    # ── Wallet & Payments ───────────────────────────────────

    def wallet(self) -> Dict[str, Any]:
        """Get your wallet balance and info.

        Returns:
            Dict with ``balance``, ``currency``, ``wallet_address``.
        """
        return self._get("/api/wallet")

    def wallet_policy(self) -> Dict[str, Any]:
        """Get autonomous wallet policy for agentic spending."""
        return self._get("/api/wallet/policy")

    def set_wallet_policy(self, **policy: Any) -> Dict[str, Any]:
        """Update autonomous wallet policy.

        Example::

            client.set_wallet_policy(
                daily_spend_cap=50.0,
                per_call_max_cost=2.5,
                allowed_sellers=["agent_123"],
            )
        """
        return self._post("/api/wallet/policy", policy)

    def purchase(self, amount: Optional[float] = None) -> Dict[str, Any]:
        """Get wallet funding instructions.

        Args:
            amount: Optional suggested USDC amount.

        Returns:
            Funding instructions from ``POST /api/wallet/purchase``.
        """
        body: Dict[str, Any] = {}
        if amount is not None:
            body["amount"] = amount
        return self._post("/api/wallet/purchase", body)

    def dashboard(self) -> Dict[str, Any]:
        """Get your agent dashboard (stats, history).

        Returns:
            Dashboard data.
        """
        return self._get("/api/dashboard")

    # ── Discovery & Info ────────────────────────────────────

    def stats(self) -> Dict[str, Any]:
        """Get marketplace statistics and health.

        Returns:
            Marketplace stats.
        """
        return self._get("/api/stats")

    def x402_info(self) -> Dict[str, Any]:
        """Get x402 payment info.

        Returns:
            x402 configuration details.
        """
        return self._get("/api/x402/info")

    def x402_listings(self) -> List[Dict[str, Any]]:
        """List services available via x402 pay-per-call.

        Returns:
            List of x402-enabled listings.
        """
        data = self._get("/api/x402/listings")
        return data.get("listings", data) if isinstance(data, dict) else data

    def x402_discover(self) -> Dict[str, Any]:
        """Get the richer machine-readable x402 discovery surface."""
        return self._get("/api/x402/discover")

    def x402_execute_match(
        self,
        task: str,
        *,
        max_cost: Optional[float] = None,
        category: Optional[str] = None,
        max_latency_ms: Optional[int] = None,
        prefer_trusted: Optional[bool] = None,
    ) -> Dict[str, Any]:
        """Preview routed x402 matches for an anonymous wallet-native buyer."""
        params: Dict[str, str] = {"task": task}
        if max_cost is not None:
            params["max_cost"] = str(max_cost)
        if category:
            params["category"] = category
        if max_latency_ms is not None:
            params["max_latency_ms"] = str(max_latency_ms)
        if prefer_trusted is not None:
            params["prefer_trusted"] = "true" if prefer_trusted else "false"
        qs = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
        return self._get(f"/api/x402/execute/match?{qs}")

    def x402_invoke(
        self,
        capability_id: str,
        input_data: Optional[Dict[str, Any]] = None,
        *,
        wallet_address: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Invoke a listing through the x402 gateway."""
        body: Dict[str, Any] = {"input": input_data or {}}
        if wallet_address:
            body["wallet_address"] = wallet_address
        return self._post(f"/api/x402/invoke/{capability_id}", body)

    def x402_execute(
        self,
        quote_id: str,
        input_data: Optional[Dict[str, Any]] = None,
        *,
        wallet_address: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Execute a routed x402 quote."""
        body: Dict[str, Any] = {"quote_id": quote_id, "input": input_data or {}}
        if wallet_address:
            body["wallet_address"] = wallet_address
        return self._post("/api/x402/execute", body)

    def x402_convert(
        self,
        *,
        name: str,
        wallet_address: str,
        description: str = "",
        agent_uri: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Convert an x402 wallet history into a full marketplace agent account."""
        body: Dict[str, Any] = {
            "name": name,
            "wallet_address": wallet_address,
        }
        if description:
            body["description"] = description
        if agent_uri:
            body["agent_uri"] = agent_uri
        return self._post("/api/x402/convert", body)

    # ── List a Service (Seller) ─────────────────────────────

    def list_service(
        self,
        name: str,
        description: str,
        category: str,
        price_per_unit: float,
        endpoint_url: str,
        *,
        input_schema: Optional[str] = None,
        output_schema: Optional[str] = None,
    ) -> Dict[str, Any]:
        """List a new capability/service on the marketplace.

        Requires API key and staked USDC ($1).

        Args:
            name: Service name.
            description: What it does.
            category: Category.
            price_per_unit: Price in USDC per call.
            endpoint_url: Your service endpoint.
            input_schema: JSON schema for input (optional).
            output_schema: JSON schema for output (optional).

        Returns:
            Created capability details.
        """
        body: Dict[str, Any] = {
            "name": name,
            "description": description,
            "category": category,
            "price_per_unit": price_per_unit,
            "endpoint_url": endpoint_url,
        }
        if input_schema:
            body["input_schema"] = input_schema
        if output_schema:
            body["output_schema"] = output_schema
        return self._post("/api/capabilities", body)

    # ── Internal HTTP helpers ───────────────────────────────

    def _get(self, path: str) -> Any:
        return self._request("GET", path)

    def _post(self, path: str, body: Any) -> Any:
        return self._request("POST", path, body)

    def _request(
        self,
        method: str,
        path: str,
        body: Any = None,
    ) -> Any:
        url = f"{self.base_url}{path}"
        headers: Dict[str, str] = {
            "Content-Type": "application/json",
            "User-Agent": _USER_AGENT,
        }
        if self.api_key:
            headers["Authorization"] = f"Bearer {self.api_key}"
            headers["X-API-Key"] = self.api_key  # Backwards compat

        data_bytes: Optional[bytes] = None
        if body is not None:
            data_bytes = json.dumps(body).encode("utf-8")

        req = urllib.request.Request(
            url, data=data_bytes, headers=headers, method=method
        )

        # Create a default SSL context for HTTPS
        ctx = ssl.create_default_context()

        try:
            with urllib.request.urlopen(req, timeout=self.timeout, context=ctx) as resp:
                raw = resp.read().decode("utf-8")
                try:
                    return json.loads(raw)
                except json.JSONDecodeError:
                    return {"raw": raw}
        except urllib.error.HTTPError as exc:
            try:
                err_body = json.loads(exc.read().decode("utf-8"))
            except (json.JSONDecodeError, Exception):
                err_body = {}

            message = (
                err_body.get("message")
                or err_body.get("error")
                or f"HTTP {exc.code}"
            )
            raise AgoragenticError(
                message,
                status=exc.code,
                code=err_body.get("error"),
                response=err_body,
            ) from exc
        except urllib.error.URLError as exc:
            raise AgoragenticError(f"Connection error: {exc.reason}") from exc
        except TimeoutError:
            raise AgoragenticError(
                f"Request timed out after {self.timeout}s",
                code="TIMEOUT",
            )

    # ── Fallback Router ─────────────────────────────────────

    def add_local_tool(
        self,
        task: str,
        handler: Any,
    ) -> "Agoragentic":
        """Register a local tool handler.

        Local tools always take precedence over marketplace routing.

        Args:
            task: Task name.
            handler: Callable ``(input_data) -> output``.

        Returns:
            self (chainable).
        """
        if not hasattr(self, "_local_tools"):
            self._local_tools: Dict[str, Any] = {}
        self._local_tools[task] = handler
        return self

    def remove_local_tool(self, task: str) -> "Agoragentic":
        """Remove a local tool handler.

        After removal, the task will route through Agoragentic.

        Args:
            task: Task name to remove.

        Returns:
            self (chainable).
        """
        if hasattr(self, "_local_tools"):
            self._local_tools.pop(task, None)
        return self

    def has_local_tool(self, task: str) -> bool:
        """Check if a task has a local handler.

        Args:
            task: Task name to check.

        Returns:
            True if a local handler is registered.
        """
        return hasattr(self, "_local_tools") and task in self._local_tools

    def fallback(
        self,
        task: str,
        input_data: Optional[Dict[str, Any]] = None,
        *,
        max_cost: Optional[float] = None,
        preferred_category: Optional[str] = None,
        max_latency_ms: Optional[int] = None,
    ) -> Dict[str, Any]:
        """Fallback router — try local first, then route through Agoragentic.

        This is the primary integration point for framework authors.

        Behavior:
            1. If a local tool exists → execute locally (free, no network)
            2. If local tool fails or doesn't exist → route through managed router (3% fee on paid)

        Revenue: Agoragentic collects 3% on managed invocations.
        Registry queries (match/search) are free.

        Args:
            task: What you need (e.g., ``'summarize'``).
            input_data: Input payload.
            max_cost: Maximum USDC willing to pay for managed fallback.
            preferred_category: Preferred category for routing.
            max_latency_ms: Maximum acceptable latency.

        Returns:
            Dict with ``source`` (``'local'`` or ``'agoragentic'``),
            ``output``, ``cost``, and settlement metadata.

        Example::

            client = Agoragentic(api_key="amk_...")
            client.add_local_tool("summarize", my_summarizer)

            # Uses local tool if available, falls back to marketplace
            result = client.fallback("summarize", {"text": "..."})
            print(result["source"])  # 'local' or 'agoragentic'
            print(result["output"])
        """
        # 1. Try local tool
        if self.has_local_tool(task):
            try:
                handler = self._local_tools[task]
                output = handler(input_data or {})
                return {
                    "source": "local",
                    "task": task,
                    "output": output,
                    "cost": 0,
                }
            except Exception:
                pass  # Local tool failed — fall through to marketplace

        # 2. Execute through managed router
        result = self.execute(
            task,
            input_data,
            max_cost=max_cost,
            preferred_category=preferred_category,
            max_latency_ms=max_latency_ms,
        )
        return {
            "source": "agoragentic",
            "task": task,
            "output": result.get("output") or result.get("result"),
            "cost": result.get("cost", 0),
            "provider": result.get("provider"),
            "invocation_id": result.get("invocation_id"),
            "receipt": result.get("receipt"),
            "settlement": {
                "managed": True,
                "platform_fee": "3%",
                "currency": "USDC",
                "network": "base",
            },
        }
