"""
CrewAI x402 plugin — pay-per-call data APIs with automatic 402 handling.

Trial mode (no key): plain unpaid calls, free quota per IP.
Paid mode (set X402_PRIVATE_KEY): on HTTP 402 the tool parses the
PAYMENT-REQUIRED header (base64 STANDARD — not base64url), signs an
EIP-3009 transferWithAuthorization on USDC/Base with your wallet, and
retries with the X-PAYMENT header. No facilitator account needed.

    pip install crewai requests eth_account
    export X402_PRIVATE_KEY=0x...   # optional; trial mode works without it

    from crewai import Agent, Task, Crew
    from crewai_x402 import X402WebSearchPaidTool, X402NewsPaidTool

    researcher = Agent(role="Market Research Analyst",
                       goal="Brief me on {topic} with live data",
                       tools=[X402WebSearchPaidTool(), X402NewsPaidTool()],
                       backstory="You use pay-per-call tools. No API keys needed.")
    task = Task(description="Research {topic}: prices + latest news", agent=researcher,
                expected_output="A short briefing with numbers and headlines")
    Crew(agents=[researcher], tasks=[task]).kickoff(inputs={"topic": "Base chain DeFi"})

Set MAX_USD_PER_CALL to cap spend per call (default $0.05).
Source: https://api.mekongtradeintel.com/tools/plugins/crewai_x402.py
"""
import base64
import json
import os
import secrets
import time

import requests

USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
BASE_URL = "https://api.mekongtradeintel.com"
UA = {"User-Agent": "x402-crewai-plugin/1.0", "Content-Type": "application/json"}
MAX_USD_PER_CALL = float(os.environ.get("X402_MAX_USD_PER_CALL", "0.05"))

try:
    from crewai.tools import BaseTool
    from pydantic import Field
except ImportError:  # pragma: no cover - readable without crewai installed
    class BaseTool:
        pass

    def Field(*a, **k):
        return None

try:
    from eth_account import Account
    from eth_account.messages import encode_typed_data
    _HAS_ETH = True
except ImportError:
    _HAS_ETH = False

_EIP712_TYPES = {
    "EIP712Domain": [
        {"name": "name", "type": "string"},
        {"name": "version", "type": "string"},
        {"name": "chainId", "type": "uint256"},
        {"name": "verifyingContract", "type": "address"},
    ],
    "TransferWithAuthorization": [
        {"name": "from", "type": "address"},
        {"name": "to", "type": "address"},
        {"name": "value", "type": "uint256"},
        {"name": "validAfter", "type": "uint256"},
        {"name": "validBefore", "type": "uint256"},
        {"name": "nonce", "type": "bytes32"},
    ],
}


def _decode_payment_required(header_value: str) -> dict:
    # CRITICAL (fleet bug fix 2026-10-20): PAYMENT-REQUIRED is base64
    # STANDARD. Do NOT use base64url here — only JWT signing uses base64url.
    return json.loads(base64.b64decode(header_value).decode("utf-8"))


def _x_payment_header(private_key: str, accepts: dict) -> str:
    if not _HAS_ETH:
        raise RuntimeError("pip install eth_account for paid mode")
    acc = accepts[0]
    if int(acc["amount"]) / 1e6 > MAX_USD_PER_CALL:
        raise RuntimeError("call price exceeds X402_MAX_USD_PER_CALL cap")
    acct = Account.from_key(private_key)
    now = int(time.time())
    auth = {
        "from": acct.address,
        "to": acc["payTo"],
        "value": int(acc["amount"]),
        "validAfter": 0,
        "validBefore": now + 600,
        "nonce": "0x" + secrets.token_hex(32),
    }
    typed = {
        "types": _EIP712_TYPES,
        "primaryType": "TransferWithAuthorization",
        "domain": {"name": "USD Coin", "version": "2", "chainId": 8453,
                   "verifyingContract": acc["asset"]},
        "message": auth,
    }
    sig = Account.sign_message(encode_typed_data(full_message=typed),
                               private_key=private_key).signature.hex()
    payment = {"x402Version": 2, "scheme": acc.get("scheme", "exact"),
               "network": acc["network"],
               "payload": {"signature": "0x" + sig, "authorization": auth}}
    return base64.b64encode(json.dumps(payment).encode()).decode()  # standard b64


def _paid_request(method: str, url: str, payload: dict | None) -> requests.Response:
    headers = dict(UA)
    data = json.dumps(payload) if payload is not None else None
    r = requests.request(method, url, headers=headers, data=data, timeout=60)
    if r.status_code != 402:
        return r
    pk = os.environ.get("X402_PRIVATE_KEY")
    if not pk:
        return r  # trial mode: surface the 402 to the caller
    header = next((v for k, v in r.headers.items()
                   if k.lower() == "payment-required"), None)
    if not header:
        return r
    accepts = _decode_payment_required(header).get("accepts") or []
    if not accepts:
        return r
    headers["X-PAYMENT"] = _x_payment_header(pk, accepts)
    return requests.request(method, url, headers=headers, data=data, timeout=60)


def _fmt_items(d: dict) -> str:
    items = d.get("results") or d.get("items") or []
    return "\n".join(f"- {(it.get('title') or it.get('headline') or '')[:120]}"
                     for it in items[:5])


class X402PaidBaseTool(BaseTool):
    """Generic x402 pay-per-call tool. Subclass and set the class attrs."""

    name: str = "x402_paid"
    description: str = "x402 pay-per-call API"
    url: str = ""
    method: str = "POST"
    price_usdc: float = 0.0
    query_arg: str = "query"

    def _run(self, query: str) -> str:
        payload = None if self.method == "GET" else {self.query_arg: query}
        r = _paid_request(self.method, self.url, payload)
        if r.status_code == 402:
            return (f"402 Payment Required (${self.price_usdc}/call). Set X402_PRIVATE_KEY "
                    "to pay in USDC on Base, or wait for trial quota reset.")
        r.raise_for_status()
        return _fmt_items(r.json())


class X402WebSearchPaidTool(X402PaidBaseTool):
    name: str = "x402_web_search"
    description: str = ("Live web search. Free trial quota, then $0.01/call in USDC on Base "
                        "(x402) — paid automatically if X402_PRIVATE_KEY is set.")
    url: str = f"{BASE_URL}/websearch/api/websearch"
    method: str = "POST"
    price_usdc: float = 0.01
    query: str = Field(description="Search query")


class X402NewsPaidTool(X402PaidBaseTool):
    name: str = "x402_news"
    description: str = ("Headlines by topic. Free trial quota, then $0.001/call in USDC on Base "
                        "(x402) — paid automatically if X402_PRIVATE_KEY is set.")
    url: str = f"{BASE_URL}/news/api/news"
    method: str = "POST"
    price_usdc: float = 0.001
    query_arg: str = "topic"
    query: str = Field(description="News topic")


if __name__ == "__main__":
    print(X402WebSearchPaidTool()._run("Base chain DeFi"))
    print(X402NewsPaidTool()._run("bitcoin"))
