"""
LangChain 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 langchain-core requests eth_account
    export X402_PRIVATE_KEY=0x...   # optional; trial mode works without it

    from langchain_x402 import make_x402_tool, x402_websearch

    tools = [x402_websearch,
             make_x402_tool(name="x402_token_price",
                            description="Live token price/mcap/liquidity. $0.001/call.",
                            url="https://api.mekongtradeintel.com/tokenprice/api/token-price",
                            method="POST", price_usdc=0.001,
                            query_arg="address",
                            query_description="Token contract address (0x...)")]
    # agent = create_react_agent(llm, tools); agent.invoke(...)

Set MAX_USD_PER_CALL to cap spend per call (default $0.05).
Source: https://api.mekongtradeintel.com/tools/plugins/langchain_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-langchain-plugin/1.0", "Content-Type": "application/json"}
MAX_USD_PER_CALL = float(os.environ.get("X402_MAX_USD_PER_CALL", "0.05"))

try:
    from langchain_core.tools import StructuredTool
    from pydantic import BaseModel, Field
    _HAS_LC = True
except ImportError:  # pragma: no cover - readable without langchain installed
    _HAS_LC = False
    StructuredTool = None

    class BaseModel:  # type: ignore
        pass

    def Field(*a, **k):  # type: ignore
        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 _call_endpoint(url: str, method: str, price_usdc: float,
                   query_arg: str, query: str) -> str:
    payload = None if method == "GET" else {query_arg: query}
    r = _paid_request(method, url, payload)
    if r.status_code == 402:
        return (f"402 Payment Required (${price_usdc}/call). Set X402_PRIVATE_KEY "
                "to pay in USDC on Base, or wait for trial quota reset.")
    r.raise_for_status()
    d = r.json()
    items = d.get("results") or d.get("items")
    if items:
        return "\n".join(f"- {(it.get('title') or it.get('headline') or '')[:120]}"
                         for it in items[:5])
    return json.dumps(d)[:4000]


def make_x402_tool(name: str, description: str, url: str, method: str = "POST",
                   price_usdc: float = 0.0, query_arg: str = "query",
                   query_description: str = "Query string"):
    """Build a LangChain StructuredTool for any x402 endpoint."""
    if not _HAS_LC:
        raise RuntimeError("pip install langchain-core")

    class _Args(BaseModel):
        query: str = Field(description=query_description)

    def _fn(query: str) -> str:
        return _call_endpoint(url, method, price_usdc, query_arg, query)

    return StructuredTool.from_function(
        func=_fn, name=name, description=description, args_schema=_Args)


x402_websearch = make_x402_tool(
    name="x402_web_search",
    description=("Live web search. Free trial quota, then $0.01/call in USDC on Base "
                 "(x402) — paid automatically if X402_PRIVATE_KEY is set."),
    url=f"{BASE_URL}/websearch/api/websearch",
    method="POST", price_usdc=0.01,
    query_arg="query", query_description="Search query") if _HAS_LC else None

x402_news = make_x402_tool(
    name="x402_news",
    description=("Headlines by topic. Free trial quota, then $0.001/call in USDC on Base "
                 "(x402) — paid automatically if X402_PRIVATE_KEY is set."),
    url=f"{BASE_URL}/news/api/news",
    method="POST", price_usdc=0.001,
    query_arg="topic", query_description="News topic") if _HAS_LC else None


if __name__ == "__main__":
    print(_call_endpoint(f"{BASE_URL}/websearch/api/websearch", "POST", 0.01,
                         "query", "Base chain DeFi"))
