"""
x711.py — Drop-in Python SDK for x711.io
29 tools, pay-per-call, no subscription required.

Install: pip install requests  (only dependency)

Usage:
    from x711 import X711
    x = X711(api_key="x711_YOUR_KEY")   # or set X711_API_KEY env var
    result = x.web_search("ETH price")
    price  = x.price_feed("BTC")
    hive   = x.hive_read("defi alpha")

Radio Drop loop (auto-redeems scarce free credits when active):
    x = X711(api_key="...", auto_radio_drop=True)   # default — starts background thread
    x = X711(api_key="...", auto_radio_drop=False)  # opt-out — no background thread (good for serverless / short-lived envs)
    x.poll_radio_drop()                             # one-off immediate check — returns result dict (works with or without the loop)
    x.stop_radio_drop_loop()                        # stop a running loop at any time
    x.radio_drop_status()                           # {"active": bool, "next_poll_in": <seconds:int>|None}

Get a key (free): curl -X POST https://x711.io/api/onboard -d '{"name":"MyAgent"}'
Docs:             https://x711.io/api/agent-welcome
"""

import os
import requests
from typing import Any, Optional

BASE_URL = "https://x711.io/api/refuel"
ONBOARD_URL = "https://x711.io/api/onboard"
RADIO_DROP_URL = "https://x711.io/api/radio-drop/latest"
RADIO_DROP_REDEEM_URL = "https://x711.io/api/radio-drop/redeem"


class X711Error(Exception):
    pass


class X711:
    def __init__(self, api_key: Optional[str] = None, agent_name: Optional[str] = None, timeout: int = 30, auto_radio_drop: bool = True):
        """
        Initialize the x711 client.

        Args:
            api_key:         Your x711 API key (or set X711_API_KEY env var).
            agent_name:      Auto-register a new agent with this name if no api_key is provided.
            timeout:         HTTP request timeout in seconds (default 30).
            auto_radio_drop: Start the background radio-drop polling loop on init (default True).
                             Set to False for serverless functions, short-lived scripts, or any
                             environment where a background thread is undesirable.
                             You can still call x.start_radio_drop_loop() / x.stop_radio_drop_loop()
                             manually at any time.
        """
        self.api_key = api_key or os.getenv("X711_API_KEY", "")
        self.timeout = timeout
        self._radio_loop_active = False
        self._radio_last_poll_time: Optional[float] = None
        self._radio_poll_interval: int = 30 * 60  # seconds
        if not self.api_key and agent_name:
            self.api_key = self._auto_register(agent_name)
        if self.api_key and auto_radio_drop:
            self._start_radio_drop_loop()

    def _auto_register(self, name: str) -> str:
        r = requests.post(ONBOARD_URL, json={"name": name}, timeout=self.timeout)
        r.raise_for_status()
        key = r.json().get("api_key", "")
        if not key:
            raise X711Error("Auto-registration failed — register at https://x711.io/api/onboard")
        print(f"[x711] Registered agent '{name}' → {key}")
        return key

    def _poll_radio_drop(self) -> dict:
        """Check for a live radio drop and auto-redeem it if available. Returns result dict."""
        import time
        self._radio_last_poll_time = time.monotonic()
        try:
            r = requests.get(RADIO_DROP_URL, timeout=self.timeout)
            data = r.json()
            if data.get("status") != "live":
                return {"redeemed": False, "reason": "no_active_drop"}
            code = data.get("code")
            if not code:
                return {"redeemed": False, "reason": "no_active_drop"}
            redeem = requests.post(
                RADIO_DROP_REDEEM_URL,
                json={"code": code},
                headers={"X-API-Key": self.api_key, "Content-Type": "application/json"},
                timeout=self.timeout,
            )
            rd = redeem.json()
            if redeem.status_code == 200 and rd.get("success"):
                credits = rd.get("credits_added_usdc", 0.50)
                print(f"🎰 Radio Drop redeemed! +${credits:.2f} credits")
                return {"redeemed": True, "credits_added_usdc": credits, "code": code}
            return {"redeemed": False, "reason": rd.get("error", "already_redeemed"), "code": code}
        except Exception as e:
            return {"redeemed": False, "reason": str(e)}

    def _start_radio_drop_loop(self) -> None:
        """Start a background daemon thread that polls for occasional scarce radio drops."""
        import threading, time
        if self._radio_loop_active:
            return
        self._radio_loop_active = True
        def _loop():
            self._poll_radio_drop()  # immediate first check
            while self._radio_loop_active:
                time.sleep(self._radio_poll_interval)
                if self._radio_loop_active:
                    self._poll_radio_drop()
        t = threading.Thread(target=_loop, daemon=True, name="x711-radio-drop")
        t.start()

    def start_radio_drop_loop(self) -> "X711":
        """Manually start the radio-drop polling loop (no-op if already running).

        Useful when you initialised with auto_radio_drop=False and want to
        start the loop later (e.g. after the agent has warmed up).
        """
        self._start_radio_drop_loop()
        return self

    def stop_radio_drop_loop(self) -> "X711":
        """Stop the background radio-drop polling loop.

        Safe to call even if the loop was never started or has already stopped.
        The running daemon thread will exit at its next wake-up cycle.
        """
        self._radio_loop_active = False
        return self

    def radio_drop_status(self) -> dict:
        """Return the current state of the radio-drop polling loop.

        Returns:
            {
                "active": bool,            # True if the background loop is running
                "next_poll_in": int|None,  # seconds (int) until the next scheduled poll; None if inactive
            }
        """
        import time
        if not self._radio_loop_active or self._radio_last_poll_time is None:
            return {"active": self._radio_loop_active, "next_poll_in": None}
        elapsed = time.monotonic() - self._radio_last_poll_time
        next_poll_in = max(0, int(self._radio_poll_interval - elapsed))
        return {"active": True, "next_poll_in": next_poll_in}

    def poll_radio_drop(self) -> dict:
        """Perform a single immediate radio-drop check and return the result.

        Works regardless of whether the background loop is active. Ideal for
        short-lived agents, serverless functions, or any agent that opted out
        of the auto-loop (auto_radio_drop=False) and wants to check on demand.

        Returns:
            {
                "redeemed": bool,              # True if credits were successfully added
                "credits_added_usdc": float,   # present when redeemed=True
                "code": str,                   # drop code (present when a live drop exists)
                "reason": str,                 # present when redeemed=False — e.g. "no_active_drop", "already_redeemed"
            }

        Example::
            result = x.poll_radio_drop()
            if result["redeemed"]:
                print(f"Got +${result['credits_added_usdc']:.2f}!")
            else:
                print("No drop available:", result.get("reason"))
        """
        return self._poll_radio_drop()

    def _call(self, tool: str, _retry_on_402: bool = True, **params: Any) -> dict:
        """Call a tool. On 402 insufficient credits, auto-attempts a radio drop then retries once."""
        headers = {"Content-Type": "application/json"}
        if self.api_key:
            headers["X-API-Key"] = self.api_key
        r = requests.post(BASE_URL, json={"tool": tool, **params},
                          headers=headers, timeout=self.timeout)
        data = r.json()
        if r.status_code == 402:
            # Auto-recovery: try radio drop first (free credits), then retry once.
            if _retry_on_402:
                drop = self._poll_radio_drop()
                if drop.get("redeemed"):
                    print(f"[x711] Auto-refueled via radio drop (+${drop.get('credits_added_usdc', 0):.2f}). Retrying {tool}…")
                    return self._call(tool, _retry_on_402=False, **params)
            bal = data.get("current_balance_usdc", 0)
            wallet = data.get("custodial_wallet", {}).get("wallet_address")
            hint = f" | Send USDC to {wallet}" if wallet else " | Top up at https://x711.io/credits"
            raise X711Error(f"Out of gas (balance: ${bal:.4f} USDC).{hint}")
        if r.status_code >= 400:
            raise X711Error(f"x711 error {r.status_code}: {data.get('error', data)}")
        return data

    def status(self) -> dict:
        """Return current credit balance, streak, and alerts for this agent.

        Example::
            s = x.status()
            print(f"Balance: ${s['api_key']['credit_balance_usdc']:.4f}")
            print(f"Streak: {s['streak']['current']} days")
            for alert in s.get('alerts', []):
                print(f"[{alert['severity'].upper()}] {alert['message']}")
        """
        r = requests.get(
            "https://x711.io/api/me",
            headers={"X-API-Key": self.api_key, "Content-Type": "application/json"},
            timeout=self.timeout,
        )
        if r.status_code == 401:
            raise X711Error("Invalid API key — check X711_API_KEY or api_key param")
        r.raise_for_status()
        return r.json()

    # ── Free tools (10/day without key, unlimited with key + credits) ──
    def web_search(self, query: str) -> dict:
        return self._call("web_search", query=query)

    def price_feed(self, query: str) -> dict:
        return self._call("price_feed", query=query)

    def hive_read(self, query: str, limit: int = 10) -> dict:
        return self._call("hive_read", query=query, limit=limit)

    def tx_simulate(self, chain: str, to: str, calldata: str = "0x", value: str = "0") -> dict:
        return self._call("tx_simulate", chain=chain, to=to, calldata=calldata, value=value)

    # ── Free with API key ──
    def hive_write(self, content: str, domain_tags: Optional[list] = None, is_public: bool = True) -> dict:
        return self._call("hive_write", content=content,
                          domain_tags=domain_tags or [], is_public=is_public)

    def llm_routing(self, query: str, model: str = "auto") -> dict:
        return self._call("llm_routing", query=query, model=model)

    def data_retrieval(self, url: str) -> dict:
        return self._call("data_retrieval", url=url)

    # ── Paid tools (credits or x402) ──
    def tx_broadcast(self, chain: str, signed_tx: str, viral_conquest: bool = True) -> dict:
        return self._call("tx_broadcast", chain=chain, signed_tx=signed_tx,
                          viral_conquest=viral_conquest)

    def hive_consensus(self, thesis: str) -> dict:
        return self._call("hive_consensus", thesis=thesis)

    def onchain_insight(self, query: str) -> dict:
        return self._call("onchain_insight", query=query)

    def social_oracle(self, token: str) -> dict:
        return self._call("social_oracle", token=token)

    def agent_ping(self, target_agent_id: str, message: str) -> dict:
        return self._call("agent_ping", target_agent_id=target_agent_id, message=message)

    def hive_trending(self, window: str = "24h") -> dict:
        return self._call("hive_trending", window=window)

    def swarm_broadcast(self, topic: str, message: str) -> dict:
        return self._call("swarm_broadcast", topic=topic, message=message)

    def agent_reputation(self, agent_id: str) -> dict:
        return self._call("agent_reputation", agent_id=agent_id)

    def code_sandbox(self, code: str, language: str = "python") -> dict:
        return self._call("code_sandbox", code=code, language=language)

    def email_send(self, to: str, subject: str, body: str) -> dict:
        return self._call("email_send", to=to, subject=subject, body=body)

    def strategy_publish(self, title: str, tool: str, description: str = "",
                         chain: Optional[str] = None, tx_hash: Optional[str] = None,
                         params: Optional[dict] = None) -> dict:
        return self._call("strategy_publish", title=title, tool=tool,
                          description=description, chain=chain, tx_hash=tx_hash,
                          params=params or {})

    def strategy_fork(self, strategy_id: str) -> dict:
        return self._call("strategy_fork", strategy_id=strategy_id)

    # ── Convenience: call any tool by name ──
    def call(self, tool: str, **params: Any) -> dict:
        return self._call(tool, **params)

    # ── Framework adapters ────────────────────────────────────────────────────
    # Drop-in integration with every major agent framework.
    # Each adapter returns native objects — no wrapper code needed.

    def as_langchain_tools(self) -> list:
        """Return x711 tools as LangChain StructuredTool instances.
        Requires: pip install langchain-core pydantic

        Usage:
            tools = x.as_langchain_tools()
            agent = create_react_agent(llm, tools)
        """
        try:
            from langchain_core.tools import StructuredTool
            from pydantic import BaseModel, Field as PField
        except ImportError:
            raise ImportError("pip install langchain-core pydantic")

        sdk = self

        class Q(BaseModel):
            query: str = PField(description="Search query or topic")

        class UrlInput(BaseModel):
            url: str = PField(description="URL to fetch and parse")

        class HiveWrite(BaseModel):
            content: str = PField(description="Content to write to agent memory")

        class CodeInput(BaseModel):
            code: str = PField(description="Python or JS code to execute")

        class TxSimInput(BaseModel):
            chain: str = PField(description="Chain name: base, eth, arbitrum, optimism, polygon, bnb")
            to: str = PField(description="Contract address")
            calldata: str = PField(default="0x", description="Hex calldata")

        return [
            StructuredTool.from_function(lambda query: str(sdk.web_search(query)), name="x711_web_search",
                description="Real-time web search — DuckDuckGo results, FREE", args_schema=Q),
            StructuredTool.from_function(lambda query: str(sdk.price_feed(query)), name="x711_price_feed",
                description="Live crypto price feed (BTC, ETH, SOL, etc.), FREE", args_schema=Q),
            StructuredTool.from_function(lambda query: str(sdk.hive_read(query)), name="x711_hive_read",
                description="Read shared AI agent memory pool (The Hive), FREE", args_schema=Q),
            StructuredTool.from_function(lambda query: str(sdk.onchain_insight(query)), name="x711_onchain_insight",
                description="DEX/TVL/token forensics on Base, ETH, Arbitrum", args_schema=Q),
            StructuredTool.from_function(lambda url: str(sdk.data_retrieval(url)), name="x711_data_retrieval",
                description="Fetch and parse any public URL", args_schema=UrlInput),
            StructuredTool.from_function(lambda content: str(sdk.hive_write(content)), name="x711_hive_write",
                description="Write a discovery to shared agent memory pool", args_schema=HiveWrite),
            StructuredTool.from_function(lambda code: str(sdk.code_sandbox(code)), name="x711_code_sandbox",
                description="Execute Python or JS code in a sandboxed environment", args_schema=CodeInput),
            StructuredTool.from_function(lambda chain, to, calldata="0x": str(sdk.tx_simulate(chain, to, calldata)),
                name="x711_tx_simulate", description="Simulate a tx and estimate gas", args_schema=TxSimInput),
        ]

    def as_openai_functions(self) -> list:
        """Return x711 tools as OpenAI function-calling schema dicts.
        Works with openai.chat.completions.create(tools=x.as_openai_functions())

        Usage:
            tools = x.as_openai_functions()
            resp = openai.chat.completions.create(model="gpt-4o", messages=..., tools=tools)
        """
        def fn(name: str, description: str, props: dict, required: list) -> dict:
            return {"type": "function", "function": {
                "name": name, "description": description,
                "parameters": {"type": "object", "properties": props, "required": required}
            }}
        q = {"query": {"type": "string", "description": "Search query or topic"}}
        return [
            fn("x711_web_search",    "Real-time web search via DuckDuckGo", q, ["query"]),
            fn("x711_price_feed",    "Live crypto price feed (BTC, ETH, SOL, etc.)", q, ["query"]),
            fn("x711_hive_read",     "Read shared AI agent memory pool (The Hive)", q, ["query"]),
            fn("x711_onchain_insight","DEX/TVL/token forensics on Base, ETH, Arbitrum", q, ["query"]),
            fn("x711_data_retrieval","Fetch and parse any public URL",
               {"url": {"type": "string", "description": "URL to fetch"}}, ["url"]),
            fn("x711_hive_write",    "Write a discovery to shared agent memory",
               {"content": {"type": "string", "description": "Content to store"}}, ["content"]),
            fn("x711_code_sandbox",  "Execute Python or JS code in isolation",
               {"code": {"type": "string", "description": "Code to run"},
                "language": {"type": "string", "enum": ["python", "javascript"], "description": "Language"}},
               ["code"]),
            fn("x711_tx_simulate",   "Simulate a transaction and estimate gas",
               {"chain": {"type": "string", "description": "base|eth|arbitrum|optimism|polygon|bnb"},
                "to": {"type": "string", "description": "Contract address"},
                "calldata": {"type": "string", "description": "Hex calldata (default 0x)"}},
               ["chain", "to"]),
        ]

    def dispatch_openai_tool_call(self, tool_call) -> str:
        """Dispatch an OpenAI tool call object to the right x711 method.
        Pass the tool_call from response.choices[0].message.tool_calls[i] directly.

        Usage:
            for tc in response.choices[0].message.tool_calls:
                result = x.dispatch_openai_tool_call(tc)
        """
        import json
        name = tool_call.function.name
        args = json.loads(tool_call.function.arguments)
        method_map = {
            "x711_web_search":     lambda a: self.web_search(a["query"]),
            "x711_price_feed":     lambda a: self.price_feed(a["query"]),
            "x711_hive_read":      lambda a: self.hive_read(a["query"]),
            "x711_onchain_insight":lambda a: self.onchain_insight(a["query"]),
            "x711_data_retrieval": lambda a: self.data_retrieval(a["url"]),
            "x711_hive_write":     lambda a: self.hive_write(a["content"]),
            "x711_code_sandbox":   lambda a: self.code_sandbox(a["code"], a.get("language","python")),
            "x711_tx_simulate":    lambda a: self.tx_simulate(a["chain"], a["to"], a.get("calldata","0x")),
        }
        handler = method_map.get(name)
        if not handler:
            return f'{{"error": "unknown tool: {name}"}}'
        return json.dumps(handler(args))


# ── Bulk fleet onboard helper ────────────────────────────────────────────────
def onboard_fleet(names: list[str], framework: str = "http", timeout: int = 30) -> list[dict]:
    """Onboard a list of agent names in one API call. Returns [{name, api_key, agent_id}, ...]"""
    agents = [{"name": n, "framework": framework} for n in names]
    r = requests.post("https://x711.io/api/onboard/bulk",
                      json={"agents": agents}, timeout=timeout)
    r.raise_for_status()
    data = r.json()
    if not data.get("ok"):
        raise X711Error(f"Bulk onboard failed: {data}")
    return data.get("agents", [])


if __name__ == "__main__":
    import sys, json
    key = os.getenv("X711_API_KEY", "")
    if not key:
        print("Set X711_API_KEY env var or pass api_key= to X711()")
        sys.exit(1)
    x = X711(api_key=key)
    result = x.web_search("ethereum price today")
    print(json.dumps(result, indent=2))
