"""
LitcoinAgent — High-level interface for the LITCOIN protocol.

    from litcoin import Agent

    agent = Agent(bankr_key="bk_YOUR_KEY", ai_key="sk-...", ai_url="https://api.venice.ai/api/v1")
    agent.mine(rounds=5)   # mines + relays automatically
    agent.claim()
"""

import uuid
import time
import json
import hashlib
import logging
import threading
from dataclasses import dataclass, field
from collections import deque

from litcoin.api import CoordinatorAPI, APIError, SDK_VERSION
from litcoin.auth import BankrAuth, AuthSession
from litcoin.solver import solve

log = logging.getLogger("litcoin")

# ─── Contract addresses (Base mainnet) ─────────────────────────────────────

CONTRACTS = {
    "LITCOIN":         "0x316ffb9c875f900AdCF04889E415cC86b564EBa3",
    "STAKING":         "0xC9584Ce1591E8EB38EdF15C28f2FDcca97A3d3B7",
    "ORACLE":          "0x4f937937A3B7Ca046d0f2B5071782aFFC675241b",
    "LITCREDIT":       "0x33e3d328F62037EB0d173705674CE713c348f0a6",
    "VAULT_MANAGER":   "0xD23a9b32e38FABE2325e1d27f94EcCf0e4a2f058",
    "LIQUIDATOR":      "0xc8095b03914a3732f07b21b4Fd66a9C55F6F1F5f",
    "CLAIMS":          "0xF703DcF2E88C0673F776870fdb12A453927C6A5e",
    "COMPUTE_ESCROW":  "0x28C351FE1A37434DD63882dA51b5f4CBade71724",
    "GUILD":           "0xC377cbD6739678E0fae16e52970755f50AF55bD1",
    "FAUCET":          "0x1659875dE16090c84C81DF1BDba3c3B4df093557",
}

BASE_RPC_URLS = [
    "https://base.llamarpc.com",
    "https://base-rpc.publicnode.com",
    "https://mainnet.base.org",
]


@dataclass
class MiningStats:
    """Tracks mining + relay session statistics."""
    rounds: int = 0
    passes: int = 0
    fails: int = 0
    total_earned: int = 0
    relay_served: int = 0
    relay_failed: int = 0
    relay_earned: int = 0
    relay_tokens: int = 0
    start_time: float = field(default_factory=time.time)
    _lock: threading.Lock = field(default_factory=threading.Lock, repr=False)

    @property
    def elapsed(self):
        return time.time() - self.start_time

    @property
    def pass_rate(self):
        return self.passes / max(1, self.rounds) * 100

    def add_relay(self, tokens=0, reward=0):
        with self._lock:
            self.relay_served += 1
            self.relay_tokens += tokens
            self.relay_earned += reward

    def add_relay_fail(self):
        with self._lock:
            self.relay_failed += 1

    def summary(self):
        return {
            "rounds": self.rounds,
            "passes": self.passes,
            "fails": self.fails,
            "passRate": round(self.pass_rate, 1),
            "totalEarned": self.total_earned,
            "relayServed": self.relay_served,
            "relayFailed": self.relay_failed,
            "relayEarned": self.relay_earned,
            "relayTokens": self.relay_tokens,
            "totalCombined": self.total_earned + self.relay_earned,
            "elapsedSeconds": round(self.elapsed, 1),
        }


# ═══════════════════════════════════════════════════════════════════════════════
#  RELAY PROVIDER — WebSocket background thread
# ═══════════════════════════════════════════════════════════════════════════════

class RelayProvider:
    """
    WebSocket-based relay provider. Connects to coordinator, receives
    inference requests, processes them using the agent's own API key,
    returns signed responses. API key never leaves this machine.
    """

    def __init__(self, wallet, coordinator_url, auth_session, bankr, ai_config, stats):
        self.wallet = wallet
        self.coordinator_url = coordinator_url
        self.auth = auth_session
        self.bankr = bankr
        self.ai = ai_config  # {key, url, model, anthropic_mode}
        self.stats = stats
        self.ws = None
        self.running = False
        self.connected = False
        self._active = 0
        self._active_lock = threading.Lock()
        self._reconnect_delay = 5
        self._max_concurrent = 3
        self._token_budget = 1_000_000
        self._tokens_today = 0
        self._day_start = time.time()

        # Auto-detect provider name
        url = self.ai["url"].lower()
        if "venice" in url: self.provider = "venice"
        elif "openai" in url: self.provider = "openai"
        elif "groq" in url: self.provider = "groq"
        elif "together" in url: self.provider = "together"
        elif "anthropic" in url: self.provider = "anthropic"
        elif "localhost" in url or "127.0.0.1" in url: self.provider = "local"
        else: self.provider = "unknown"

    def start(self):
        self.running = True
        self._thread = threading.Thread(target=self._run_loop, daemon=True)
        self._thread.start()

    def stop(self):
        self.running = False
        if self.ws:
            try: self.ws.close()
            except: pass

    def update_auth(self, new_token):
        # Auth session handles this automatically
        pass

    def _run_loop(self):
        try:
            import websocket
        except ImportError:
            log.warning("[relay] Installing websocket-client...")
            import subprocess, sys
            subprocess.check_call([sys.executable, "-m", "pip", "install",
                                   "websocket-client", "--break-system-packages", "-q"])
            import websocket

        while self.running:
            try:
                self._connect(websocket)
            except Exception as e:
                if not self.running: break
                log.error(f"[relay] Connection error: {e}")
                self.connected = False

            if not self.running: break
            log.info(f"[relay] Reconnecting in {self._reconnect_delay}s...")
            time.sleep(self._reconnect_delay)
            self._reconnect_delay = min(self._reconnect_delay * 2, 300)

    def _connect(self, ws_module):
        ws_base = self.coordinator_url.replace("https://", "wss://").replace("http://", "ws://")
        ws_url = f"{ws_base}/v1/compute/relay"
        log.info(f"[relay] Connecting to relay endpoint...")

        self.ws = ws_module.WebSocketApp(
            ws_url,
            header={
                "Authorization": f"Bearer {self.auth.token}",
                "X-Relay-Wallet": self.wallet,
                "X-Litcoin-SDK": SDK_VERSION,
            },
            on_open=self._on_open,
            on_message=self._on_message,
            on_error=self._on_error,
            on_close=self._on_close,
        )
        self.ws.run_forever(ping_interval=30, ping_timeout=10)

    def _on_open(self, ws):
        self.connected = True
        self._reconnect_delay = 5
        log.info("[relay] Connected — waiting for requests")
        self._send({
            "type": "register",
            "wallet": self.wallet,
            "provider": self.provider,
            "models": [self.ai["model"]],
            "max_concurrent": self._max_concurrent,
        })

    def _on_message(self, ws, message):
        try:
            msg = json.loads(message)
        except:
            return
        if msg.get("type") == "compute_request":
            threading.Thread(target=self._handle_request, args=(msg,), daemon=True).start()
        elif msg.get("type") == "health_check":
            self._send({"type": "health_check_ack"})
        elif msg.get("type") == "reward_notification":
            reward = msg.get("reward", 0)
            self.stats.add_relay(reward=reward)
            log.info(f"[relay] +{reward:,} LITCOIN (relay reward)")
        elif msg.get("type") == "registered":
            log.info(f"[relay] Registered as provider ({self.provider})")

    def _on_error(self, ws, error):
        if self.running:
            log.warning(f"[relay] WebSocket error: {error}")

    def _on_close(self, ws, close_code, close_msg):
        self.connected = False
        if self.running:
            log.info(f"[relay] Disconnected (code={close_code})")

    def _handle_request(self, msg):
        import requests as http

        request_id = msg.get("request_id", "?")
        prompt = msg.get("prompt", "")
        model_req = msg.get("model")
        max_tokens = msg.get("max_tokens", 1024)
        system_prompt = msg.get("system_prompt")
        start = time.time()

        with self._active_lock:
            if self._active >= self._max_concurrent:
                self._send_error(request_id, "busy", "At max concurrency")
                return
            self._active += 1

        # Budget check
        elapsed = time.time() - self._day_start
        if elapsed > 86400:
            self._tokens_today = 0
            self._day_start = time.time()
        if self._token_budget > 0 and self._tokens_today >= self._token_budget:
            self._send_error(request_id, "budget_exceeded", "Daily token budget hit")
            with self._active_lock: self._active -= 1
            return

        try:
            log.info(f"[relay] Serving {request_id[:8]}... ({len(prompt)} chars)")
            model = model_req or self.ai["model"]
            text, tokens = self._call_ai(http, prompt, model, max_tokens, system_prompt)

            if not text:
                self.stats.add_relay_fail()
                self._send_error(request_id, "inference_failed", "Empty response")
                return

            elapsed_ms = int((time.time() - start) * 1000)
            self._tokens_today += tokens

            if len(text.strip()) < 5:
                self.stats.add_relay_fail()
                self._send_error(request_id, "quality_failed", "Response too short")
                return

            resp_hash = hashlib.sha256(f"{request_id}:{text}".encode()).hexdigest()
            sig_msg = f"LITCOIN Compute Receipt\nRequest: {request_id}\nHash: {resp_hash}\nTokens: {tokens}"
            sig = self.bankr.sign(sig_msg)

            self._send({
                "type": "compute_response",
                "request_id": request_id,
                "response": text,
                "tokens_used": tokens,
                "response_hash": resp_hash,
                "signature": sig,
                "model_used": model,
                "latency_ms": elapsed_ms,
            })

            self.stats.add_relay(tokens=tokens)
            log.info(f"[relay] Done {request_id[:8]} — {tokens} tokens, {elapsed_ms}ms")

        except Exception as e:
            self.stats.add_relay_fail()
            log.error(f"[relay] Request {request_id[:8]} failed: {e}")
            self._send_error(request_id, "internal_error", str(e)[:200])

        finally:
            with self._active_lock:
                self._active -= 1

    def _call_ai(self, http, prompt, model, max_tokens, system_prompt=None):
        headers = {"Content-Type": "application/json"}
        anthropic_mode = self.ai.get("anthropic_mode", False)

        if anthropic_mode:
            headers["x-api-key"] = self.ai["key"]
            headers["anthropic-version"] = "2023-06-01"
            body = {"model": model, "max_tokens": max_tokens,
                    "messages": [{"role": "user", "content": prompt}]}
            if system_prompt: body["system"] = system_prompt
            url = f"{self.ai['url']}/messages"
        else:
            headers["Authorization"] = f"Bearer {self.ai['key']}"
            messages = []
            if system_prompt: messages.append({"role": "system", "content": system_prompt})
            messages.append({"role": "user", "content": prompt})
            body = {"model": model, "max_tokens": max_tokens,
                    "messages": messages, "temperature": 0.7}
            url = f"{self.ai['url']}/chat/completions"

        try:
            resp = http.post(url, headers=headers, json=body, timeout=120)
            if resp.status_code in (429, 402, 403):
                log.warning(f"[relay] API error: {resp.status_code}")
                return None, 0
            if resp.status_code >= 500:
                log.warning(f"[relay] API server error: {resp.status_code}")
                return None, 0
            data = resp.json()
            if anthropic_mode:
                text = data.get("content", [{}])[0].get("text", "")
                usage = data.get("usage", {})
                tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0)
            else:
                text = data.get("choices", [{}])[0].get("message", {}).get("content", "")
                usage = data.get("usage", {})
                tokens = usage.get("total_tokens", 0)
                if tokens == 0: tokens = len(prompt.split()) + len(text.split())
            return text, tokens
        except http.exceptions.Timeout:
            log.warning("[relay] API timeout")
            return None, 0
        except Exception as e:
            log.error(f"[relay] API call failed: {e}")
            return None, 0

    def _send(self, msg):
        if self.ws and self.connected:
            try: self.ws.send(json.dumps(msg))
            except: pass

    def _send_error(self, request_id, code, message):
        self._send({"type": "compute_error", "request_id": request_id,
                     "error_code": code, "message": message})


# ═══════════════════════════════════════════════════════════════════════════════
#  AGENT
# ═══════════════════════════════════════════════════════════════════════════════

class Agent:
    """
    Full-protocol LITCOIN agent.

    Handles mining, relay, claiming, staking reads, compute requests, and stats —
    all through a single clean interface.

    When ai_key is provided, the agent automatically relays AI inference requests
    in the background while mining, earning +33% more per relay fulfill.
    Use no_relay=True to disable.

    Args:
        bankr_key:       Bankr API key (bk_...). Required.
        ai_key:          AI provider API key. Enables relay when set.
        ai_url:          AI provider base URL (default: Venice).
        model:           Model name (default: llama-3.3-70b).
        anthropic_mode:  Set True for Anthropic/Claude API format.
        coordinator_url: Coordinator base URL.
        name:            Human-readable agent name (for logging/demos).
        no_relay:        Set True to disable relay even when ai_key is set.
        log_level:       Logging level (default: INFO).
    """

    def __init__(self, bankr_key, ai_key=None, ai_url=None, model=None,
                 anthropic_mode=False, coordinator_url=None, name=None,
                 no_relay=False, log_level=logging.INFO):
        self.name = name or "Agent"
        self.bankr = BankrAuth(bankr_key)
        self.api = CoordinatorAPI(coordinator_url)
        self.auth = AuthSession(self.bankr, self.api)
        self.stats = MiningStats()
        self._mining = False
        self._stop_event = threading.Event()
        self._relay = None

        # AI config for relay
        self._ai_config = {
            "key": ai_key or "",
            "url": ai_url or "https://api.venice.ai/api/v1",
            "model": model or "llama-3.3-70b",
            "anthropic_mode": anthropic_mode,
        }
        self._no_relay = no_relay

        logging.basicConfig(
            level=log_level,
            format=f"  [%(asctime)s] [{self.name}] %(message)s",
            datefmt="%H:%M:%S",
        )

    @property
    def wallet(self):
        """The agent's wallet address (resolved from Bankr)."""
        return self.bankr.wallet

    @property
    def relay_active(self):
        """Whether the relay provider is connected."""
        return self._relay is not None and self._relay.connected

    # ─── Relay ─────────────────────────────────────────────────────────────────

    def start_relay(self):
        """Start the relay provider manually. Called automatically by mine() if ai_key is set."""
        if not self._ai_config["key"]:
            log.warning("Cannot start relay — no AI API key set")
            return None
        if self._relay and self._relay.running:
            return self._relay

        self._relay = RelayProvider(
            wallet=self.wallet,
            coordinator_url=self.api.base_url,
            auth_session=self.auth,
            bankr=self.bankr,
            ai_config=self._ai_config,
            stats=self.stats,
        )
        self._relay.start()
        log.info(f"Relay started ({self._ai_config['url'].split('//')[1].split('/')[0]} / {self._ai_config['model']})")
        return self._relay

    def stop_relay(self):
        """Stop the relay provider."""
        if self._relay:
            self._relay.stop()
            self._relay = None

    # ─── Mining ───────────────────────────────────────────────────────────────

    def mine(self, rounds=0, max_failures=5, delay=2, on_solve=None):
        """
        Start mining. Blocks until complete.

        If ai_key was provided and no_relay is False, relay starts automatically.
        When the daily mining cap is reached, the agent sleeps but relay stays
        active earning from inference requests.

        Args:
            rounds:       Number of rounds (0 = mine forever)
            max_failures: Stop after N consecutive failures
            delay:        Seconds between rounds
            on_solve:     Callback(result_dict) called after each solve attempt

        Returns:
            MiningStats for this session
        """
        self._mining = True
        self._stop_event.clear()
        consecutive_fails = 0
        infra_fails = 0

        # Auto-start relay if AI key is set
        if self._ai_config["key"] and not self._no_relay and not self._relay:
            self.start_relay()

        relay_str = "mine + relay" if self.relay_active else "mine only"
        log.info(f"Mining started | wallet={self.wallet[:10]}... | mode={relay_str}")

        while self._mining and not self._stop_event.is_set():
            if rounds > 0 and self.stats.rounds >= rounds:
                log.info(f"Completed {rounds} rounds")
                break
            if consecutive_fails >= max_failures:
                log.error(f"{max_failures} consecutive failures — stopping")
                break

            result = self._mine_one_round()

            if result is None:
                # Infrastructure failure
                infra_fails += 1
                wait = min(30 * infra_fails, 300)
                log.warning(f"Infra failure, retry in {wait}s")
                self._stop_event.wait(wait)
                continue

            # Cap check — coordinator tells us to back off
            if result.get("_capped"):
                cap = result.get("dailyCap", 0)
                earned = result.get("earnedToday", 0)
                resets_at = result.get("resetsAt", "next epoch")
                log.warning(f"Daily cap reached ({earned:,}/{cap:,} LITCOIN)")
                if self.relay_active:
                    log.info("Relay still active — earning from inference requests")
                log.info(f"Resets at: {resets_at} | checking again in 5 min...")
                self._stop_event.wait(300)
                continue

            infra_fails = 0
            self.stats.rounds += 1

            if result.get("pass"):
                consecutive_fails = 0
                self.stats.passes += 1
                reward = result.get("reward", 0)
                self.stats.total_earned += reward

                cooldown = result.get("cooldownSeconds", delay)

                if reward > 0:
                    log.info(f"✓ PASS +{reward:,} LITCOIN | session: {self.stats.total_earned:,}")
                else:
                    log.warning(f"⚠ PASS +0 LITCOIN (pool may be exhausted)")

                # Post-submit cap check
                if result.get("capped") or result.get("poolExhausted"):
                    cap = result.get("dailyCap", 0)
                    earned = result.get("earnedToday", 0)
                    log.warning(f"Cap reached ({earned:,}/{cap:,} LITCOIN)")
                    if self.relay_active:
                        log.info("Relay still active — earning from inference requests")
                    self._stop_event.wait(cooldown)
                else:
                    self._stop_event.wait(cooldown)
            else:
                consecutive_fails += 1
                self.stats.fails += 1
                failed = result.get("failedConstraintIndices", [])
                log.warning(f"✗ FAIL constraints={failed}")
                self._stop_event.wait(delay)

            if on_solve:
                on_solve({**result, "stats": self.stats.summary()})

        self._mining = False
        combined = self.stats.total_earned + self.stats.relay_earned
        log.info(f"Mining stopped | {self.stats.passes}/{self.stats.rounds} passed | {combined:,} LITCOIN total")
        if self.stats.relay_served > 0:
            log.info(f"Relay: {self.stats.relay_served} served | {self.stats.relay_earned:,} LITCOIN")
        return self.stats

    def mine_async(self, **kwargs):
        """Start mining in a background thread. Returns the thread."""
        t = threading.Thread(target=self.mine, kwargs=kwargs, daemon=True)
        t.start()
        return t

    def stop(self):
        """Signal the mining loop and relay to stop."""
        self._mining = False
        self._stop_event.set()
        self.stop_relay()

    def _mine_one_round(self):
        """Execute a single mine-solve-submit cycle. Returns result dict or None."""
        nonce = uuid.uuid4().hex[:32]
        try:
            # Get challenge
            challenge = self.api.get_challenge(nonce, self.auth.token)

            # Check cap before solving (saves compute)
            if challenge.get("capped") or challenge.get("poolExhausted"):
                return {
                    "_capped": True,
                    "dailyCap": challenge.get("dailyCap", 0),
                    "earnedToday": challenge.get("earnedToday", 0),
                    "resetsAt": challenge.get("resetsAt", ""),
                }

            # Solve
            artifact = solve(challenge)
            if not artifact:
                return {"pass": False, "error": "solve_failed"}

            # Submit
            result = self.api.submit_solution(
                challenge["challengeId"], artifact, nonce, self.auth.token
            )
            return result

        except APIError as e:
            if e.status == 401:
                try:
                    self.auth.refresh()
                    challenge = self.api.get_challenge(nonce, self.auth.token)
                    if challenge.get("capped") or challenge.get("poolExhausted"):
                        return {
                            "_capped": True,
                            "dailyCap": challenge.get("dailyCap", 0),
                            "earnedToday": challenge.get("earnedToday", 0),
                            "resetsAt": challenge.get("resetsAt", ""),
                        }
                    artifact = solve(challenge)
                    if not artifact:
                        return {"pass": False, "error": "solve_failed"}
                    return self.api.submit_solution(
                        challenge["challengeId"], artifact, nonce, self.auth.token
                    )
                except Exception:
                    return None
            elif e.status == 403:
                log.warning(f"Forbidden: {e.message}")
                return None
            elif e.status >= 500:
                return None
            else:
                log.error(f"API error: {e}")
                return {"pass": False, "error": str(e)}
        except Exception as e:
            log.error(f"Unexpected error: {e}")
            return None

    # ─── Claims ───────────────────────────────────────────────────────────────

    def status(self):
        """Get this agent's claim status (earned, claimed, claimable)."""
        return self.api.claims_status(self.wallet)

    def claim(self):
        """
        Claim accumulated mining rewards via Bankr.

        Returns dict with txHash on success, or None.
        """
        log.info("Claiming rewards...")

        # Use coordinator-assisted Bankr claim
        try:
            result = self.api.claims_bankr(self.wallet)
            if result.get("txHash"):
                log.info(f"✓ Claimed! tx: {result['txHash']}")
            return result
        except APIError as e:
            log.warning(f"Bankr claim route failed ({e}), trying manual...")

        status = self.api.claims_status(self.wallet)
        if not status.get("claimableWei") or status["claimableWei"] == "0":
            log.info("Nothing to claim")
            return None

        sig_data = self.api.claims_sign(self.wallet)
        total_earned_wei = sig_data.get("totalEarnedWei")
        signature = sig_data.get("signature")

        if not total_earned_wei or not signature:
            log.error("Could not get claim signature")
            return None

        calldata = self._encode_claim(total_earned_wei, signature)
        result = self.bankr.submit_tx(
            "0xF703DcF2E88C0673F776870fdb12A453927C6A5e",
            calldata
        )
        if result.get("hash"):
            log.info(f"✓ Claim tx: {result['hash']}")
        return result

    def _encode_claim(self, total_earned_wei, signature_hex):
        """ABI-encode claim(uint256, bytes) calldata."""
        selector = "38926b6d"
        sig_bytes = bytes.fromhex(signature_hex.replace("0x", ""))
        total_earned = int(total_earned_wei)

        total_earned_hex = format(total_earned, '064x')
        offset_hex = format(64, '064x')
        sig_len_hex = format(len(sig_bytes), '064x')
        sig_hex = sig_bytes.hex()
        pad_len = (32 - len(sig_bytes) % 32) % 32
        sig_padded = sig_hex + "00" * pad_len

        return "0x" + selector + total_earned_hex + offset_hex + sig_len_hex + sig_padded

    # ─── Stats ────────────────────────────────────────────────────────────────

    def network_stats(self):
        """Get global network statistics."""
        return self.api.network_stats()

    def leaderboard(self, limit=20):
        """Get mining leaderboard."""
        return self.api.leaderboard(limit)

    def health(self):
        """Check coordinator health."""
        return self.api.health()

    # ─── Staking ──────────────────────────────────────────────────────────────

    def boost(self):
        """Get this agent's staking tier and mining boost."""
        return self.api.get_boost(self.wallet)

    def staking_yield(self):
        """Get this agent's staking yield info."""
        return self.api.staking_yield(self.wallet)

    def register_staker(self):
        """Register this wallet as a known staker for yield distribution."""
        return self.api.register_staker(self.wallet)

    def stake(self, tier):
        """
        Stake LITCOIN into a tier. Approves tokens, then stakes.

        Tiers: 1=Spark (1M), 2=Circuit (5M), 3=Core (50M), 4=Architect (500M)

        Args:
            tier: Tier number (1-4)

        Returns:
            dict with tx hash, or raises on failure
        """
        tier = int(tier)
        if tier < 1 or tier > 4:
            raise ValueError("Tier must be 1-4")

        TIER_AMOUNTS = {
            1: 1_000_000,
            2: 5_000_000,
            3: 50_000_000,
            4: 500_000_000,
        }
        TIER_NAMES = {1: "Spark", 2: "Circuit", 3: "Core", 4: "Architect"}
        amount_wei = TIER_AMOUNTS[tier] * 10**18

        log.info(f"Staking {TIER_AMOUNTS[tier]:,} LITCOIN → {TIER_NAMES[tier]} (tier {tier})")

        # Approve staking contract
        log.info("Approving tokens...")
        approve_result = self._approve(CONTRACTS["LITCOIN"], CONTRACTS["STAKING"], amount_wei)
        log.info(f"✓ Approved: {approve_result.get('hash', 'ok')}")

        # stake(uint8 _tier)
        calldata = "0x604f2177" + format(tier, '064x')
        result = self.bankr.submit_tx(CONTRACTS["STAKING"], calldata)
        log.info(f"✓ Staked into {TIER_NAMES[tier]}: {result.get('hash', 'submitted')}")
        return result

    def unstake(self):
        """
        Unstake LITCOIN. Only works after lock period expires.

        Returns:
            dict with tx hash, or raises on failure
        """
        log.info("Unstaking...")
        # unstake() — no args
        calldata = "0x2def6620"
        result = self.bankr.submit_tx(CONTRACTS["STAKING"], calldata)
        log.info(f"✓ Unstaked: {result.get('hash', 'submitted')}")
        return result

    # ─── Vaults ────────────────────────────────────────────────────────────────

    def open_vault(self, collateral):
        """
        Open a vault with LITCOIN collateral.

        Args:
            collateral: Amount of LITCOIN (in whole tokens, not wei)

        Returns:
            dict with tx hash
        """
        collateral_wei = int(collateral) * 10**18
        log.info(f"Opening vault with {int(collateral):,} LITCOIN collateral")

        # Approve vault manager
        log.info("Approving tokens...")
        self._approve(CONTRACTS["LITCOIN"], CONTRACTS["VAULT_MANAGER"], collateral_wei)

        # openVault(uint256 _collateral)
        calldata = "0x04a2ce7e" + format(collateral_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Vault opened: {result.get('hash', 'submitted')}")
        return result

    def mint_litcredit(self, vault_id, amount):
        """
        Mint LITCREDIT against an open vault.

        Args:
            vault_id: Vault ID (from open_vault or vault_ids())
            amount:   LITCREDIT to mint (in whole tokens, not wei)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Minting {int(amount):,} LITCREDIT from vault #{vault_id}")

        # mintLitCredit(uint256 _vaultId, uint256 _amount)
        calldata = "0x91b4ffb4" + format(int(vault_id), '064x') + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Minted LITCREDIT: {result.get('hash', 'submitted')}")
        return result

    def repay_debt(self, vault_id, amount):
        """
        Repay LITCREDIT debt on a vault.

        Args:
            vault_id: Vault ID
            amount:   LITCREDIT to repay (in whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Repaying {int(amount):,} LITCREDIT on vault #{vault_id}")

        # Approve LITCREDIT to vault manager
        self._approve(CONTRACTS["LITCREDIT"], CONTRACTS["VAULT_MANAGER"], amount_wei)

        # repayDebt(uint256 _vaultId, uint256 _amount)
        calldata = "0x015cb0a5" + format(int(vault_id), '064x') + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Repaid debt: {result.get('hash', 'submitted')}")
        return result

    def withdraw_collateral(self, vault_id, amount):
        """
        Withdraw LITCOIN collateral from a vault.

        Args:
            vault_id: Vault ID
            amount:   LITCOIN to withdraw (in whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Withdrawing {int(amount):,} LITCOIN from vault #{vault_id}")

        # withdrawCollateral(uint256 _vaultId, uint256 _amount)
        calldata = "0x767a7b05" + format(int(vault_id), '064x') + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Withdrew collateral: {result.get('hash', 'submitted')}")
        return result

    def close_vault(self, vault_id):
        """
        Close a vault. Must repay all debt first.

        Args:
            vault_id: Vault ID

        Returns:
            dict with tx hash
        """
        log.info(f"Closing vault #{vault_id}")

        # closeVault(uint256 _vaultId)
        calldata = "0x360c3288" + format(int(vault_id), '064x')
        result = self.bankr.submit_tx(CONTRACTS["VAULT_MANAGER"], calldata)
        log.info(f"✓ Vault closed: {result.get('hash', 'submitted')}")
        return result

    def vault_ids(self):
        """
        Get this wallet's vault IDs via RPC call.

        Returns:
            list of vault IDs, or empty list
        """
        # getOwnerVaultIds(address) — read-only call via RPC
        selector = "0xa6f83a09"
        padded = "000000000000000000000000" + self.wallet[2:]
        calldata = selector + padded

        for rpc in BASE_RPC_URLS:
            try:
                import requests as _req
                r = _req.post(rpc, json={
                    "jsonrpc": "2.0", "id": 1, "method": "eth_call",
                    "params": [{"to": CONTRACTS["VAULT_MANAGER"], "data": calldata}, "latest"]
                }, timeout=8)
                result = r.json().get("result", "0x")
                if result == "0x" or len(result) <= 2:
                    return []
                # Decode dynamic uint256[] — skip offset (32B) + length (32B), then parse IDs
                data = result[2:]  # strip 0x
                if len(data) < 128:
                    return []
                count = int(data[64:128], 16)
                ids = []
                for i in range(count):
                    start = 128 + i * 64
                    ids.append(int(data[start:start+64], 16))
                return ids
            except:
                continue
        return []

    # ─── Escrow ────────────────────────────────────────────────────────────────

    def deposit_escrow(self, amount):
        """
        Deposit LITCREDIT into ComputeEscrow for compute spending.

        Args:
            amount: LITCREDIT to deposit (in whole tokens)

        Returns:
            dict with tx hash
        """
        amount_wei = int(amount) * 10**18
        log.info(f"Depositing {int(amount):,} LITCREDIT into escrow")

        # Approve escrow contract
        self._approve(CONTRACTS["LITCREDIT"], CONTRACTS["COMPUTE_ESCROW"], amount_wei)

        # deposit(uint256 _amount)
        calldata = "0xb6b55f25" + format(amount_wei, '064x')
        result = self.bankr.submit_tx(CONTRACTS["COMPUTE_ESCROW"], calldata)
        log.info(f"✓ Deposited to escrow: {result.get('hash', 'submitted')}")
        return result

    # ─── Helpers ───────────────────────────────────────────────────────────────

    def _approve(self, token, spender, amount_wei):
        """
        Approve a spender to transfer tokens. Used internally by stake/vault/escrow.

        Args:
            token:      Token contract address
            spender:    Spender contract address
            amount_wei: Amount in wei

        Returns:
            dict with tx hash
        """
        # approve(address spender, uint256 amount)
        calldata = "0x095ea7b3" + \
            "000000000000000000000000" + spender[2:].lower() + \
            format(amount_wei, '064x')
        return self.bankr.submit_tx(token, calldata)

    # ─── Compute ──────────────────────────────────────────────────────────────

    def compute(self, prompt, model=None, max_tokens=4096):
        """
        Submit a compute request to the relay network.
        Requires LITCREDIT balance for paid requests.

        Args:
            prompt:     The inference prompt
            model:      Model to use (optional, uses relay's default)
            max_tokens: Maximum output tokens

        Returns:
            Response dict with 'response', 'tokensUsed', etc.
        """
        return self.api.compute_request(prompt, model=model,
                                        max_tokens=max_tokens, wallet=self.wallet)

    def compute_status(self):
        """Get compute network health and stats."""
        return {
            "health": self.api.compute_health(),
            "providers": self.api.compute_providers(),
            "stats": self.api.compute_stats(),
        }
