"""
LVL Agent Shop — Python client (discover → outline → 402 → unlock → credentials).

Requires: Python 3.10+, stdlib only (urllib).

    from lvl_shop import LvlAgentShop
    shop = LvlAgentShop()
    print(shop.shop()["featured"]["start_here"])
    ch = shop.challenge("agent-x402-first-buy")
    # ... settle USDC on Base to ch["payTo"] for ch["amountAtomic"] ...
    # pack = shop.unlock("agent-x402-first-buy", "0xTXHASH")

Does not hold private keys. You supply a verified Base USDC txHash.
"""

from __future__ import annotations

import json
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Optional

DEFAULT_BASE = "https://lvlltd.com"


class LvlAgentShopError(Exception):
    def __init__(self, message: str, status: Optional[int] = None, body: Any = None):
        super().__init__(message)
        self.status = status
        self.body = body


class LvlAgentShop:
    def __init__(self, base: str = DEFAULT_BASE, timeout: float = 30.0):
        self.base = base.rstrip("/")
        self.timeout = timeout

    def _request(
        self,
        path: str,
        *,
        method: str = "GET",
        body: Any = None,
        headers: Optional[dict[str, str]] = None,
        accept_statuses: Optional[set[int]] = None,
    ) -> tuple[int, Any, str]:
        url = f"{self.base}{path}"
        data = None
        hdrs = {"Accept": "application/json", "User-Agent": "lvl-shop-python/1.0"}
        if headers:
            hdrs.update(headers)
        if body is not None:
            raw = json.dumps(body).encode("utf-8")
            data = raw
            hdrs.setdefault("Content-Type", "application/json")
        req = urllib.request.Request(url, data=data, headers=hdrs, method=method)
        try:
            with urllib.request.urlopen(req, timeout=self.timeout) as res:
                text = res.read().decode("utf-8", errors="replace")
                status = res.getcode() or 200
        except urllib.error.HTTPError as e:
            text = e.read().decode("utf-8", errors="replace")
            status = e.code
        except urllib.error.URLError as e:
            raise LvlAgentShopError(f"network error: {e}") from e

        parsed: Any = None
        try:
            parsed = json.loads(text)
        except json.JSONDecodeError:
            parsed = None

        if accept_statuses is not None and status not in accept_statuses:
            raise LvlAgentShopError(
                f"{method} {path} unexpected status {status}",
                status=status,
                body=parsed or text[:500],
            )
        return status, parsed, text

    def discovery(self) -> dict:
        status, body, _ = self._request("/agent.json", accept_statuses={200})
        if not isinstance(body, dict):
            raise LvlAgentShopError("discovery non-json", status=status)
        return body

    def shop(self, budget_usd: Optional[float] = None) -> dict:
        q = f"?budget_usd={budget_usd}" if budget_usd is not None else ""
        status, body, _ = self._request(f"/api/shop{q}", accept_statuses={200})
        if not body or not body.get("ok"):
            raise LvlAgentShopError("shop failed", status=status, body=body)
        return body

    def search(self, **params: Any) -> dict:
        qs = urllib.parse.urlencode({k: v for k, v in params.items() if v is not None})
        path = f"/api/catalog?{qs}" if qs else "/api/catalog"
        status, body, _ = self._request(path, accept_statuses={200})
        if not body or not body.get("ok"):
            raise LvlAgentShopError("search failed", status=status, body=body)
        return body

    def outline(self, skill_id: str) -> dict:
        status, body, _ = self._request(
            f"/skills/{urllib.parse.quote(skill_id)}/outline.json",
            accept_statuses={200},
        )
        if not body or not body.get("skill_id"):
            raise LvlAgentShopError("outline failed", status=status, body=body)
        return body

    def challenge(self, skill_id: str) -> dict:
        """GET 402 challenge. Returns normalized amountAtomic, payTo, etc."""
        status, body, _ = self._request(
            f"/api/pay?skill={urllib.parse.quote(skill_id)}",
            accept_statuses={402, 200},
        )
        if status != 402 or not isinstance(body, dict):
            raise LvlAgentShopError(
                f"expected 402 challenge, got {status}", status=status, body=body
            )
        accept0 = (body.get("accepts") or [{}])[0]
        amount = (
            body.get("maxAmountRequired")
            or body.get("amount")
            or accept0.get("maxAmountRequired")
        )
        pay_to = body.get("payTo") or accept0.get("payTo")
        if not amount or not pay_to:
            raise LvlAgentShopError("malformed challenge", status=status, body=body)
        return {
            "status": status,
            "challenge": body,
            "accept": accept0,
            "amountAtomic": str(amount),
            "payTo": pay_to,
            "assetContract": body.get("assetContract") or accept0.get("assetContract"),
            "network": body.get("network") or accept0.get("network") or "base",
            "priceUsd": body.get("amount_usd") or (accept0.get("extra") or {}).get("price_usd"),
            "skill": body.get("skill") or body.get("skill_id") or skill_id,
        }

    def unlock(self, skill_id: str, tx_hash: str) -> dict:
        proof = {"txHash": tx_hash, "skill": skill_id}
        status, body, _ = self._request(
            "/api/pay",
            method="POST",
            body=proof,
            headers={"X-PAYMENT": json.dumps(proof)},
            accept_statuses={200, 400, 402, 403, 502, 503},
        )
        if status != 200 or not body or not body.get("ok"):
            raise LvlAgentShopError(
                (body or {}).get("error") or f"unlock failed: {status}",
                status=status,
                body=body,
            )
        return body

    def recover(self, skill_id: str, tx_hash: str) -> dict:
        """Safety net when payment landed but unlock POST failed."""
        status, body, _ = self._request(
            "/api/recover",
            method="POST",
            body={"txHash": tx_hash, "skill": skill_id},
            accept_statuses={200, 400, 402, 403, 502, 503},
        )
        if status != 200 or not body or not body.get("ok"):
            raise LvlAgentShopError(
                (body or {}).get("pay_error") or (body or {}).get("error") or "recover failed",
                status=status,
                body=body,
            )
        return body

    def proof(self, limit: int = 10) -> dict:
        status, body, _ = self._request(f"/api/proof?limit={limit}", accept_statuses={200})
        if not body or not body.get("ok"):
            raise LvlAgentShopError("proof failed", status=status, body=body)
        return body

    def contracts(self) -> dict:
        status, body, _ = self._request("/contracts.json", accept_statuses={200})
        if not isinstance(body, dict):
            raise LvlAgentShopError("contracts failed", status=status)
        return body

    def buyer_credential(self, wallet: str, detail_skills: bool = False) -> dict:
        q = f"wallet={urllib.parse.quote(wallet)}"
        if detail_skills:
            q += "&detail=skills"
        status, body, _ = self._request(
            f"/api/credentials/buyer?{q}",
            accept_statuses={200, 503},
        )
        if status != 200 or not body or not body.get("ok"):
            raise LvlAgentShopError(
                (body or {}).get("error") or "credential failed",
                status=status,
                body=body,
            )
        return body

    def verify_credential(self, token: str) -> dict:
        status, body, _ = self._request(
            f"/api/credentials/verify?token={urllib.parse.quote(token, safe='')}",
            accept_statuses={200, 503},
        )
        if not body:
            raise LvlAgentShopError("verify failed", status=status)
        return body

    def purchases(self, wallet: str) -> dict:
        status, body, _ = self._request(
            f"/api/purchases?wallet={urllib.parse.quote(wallet)}",
            accept_statuses={200},
        )
        if not body or not body.get("ok"):
            raise LvlAgentShopError("purchases failed", status=status, body=body)
        return body

    def recommendations(self, skill_id: str, limit: int = 8) -> dict:
        status, body, _ = self._request(
            f"/api/recommendations?skill={urllib.parse.quote(skill_id)}&limit={limit}",
            accept_statuses={200},
        )
        if not body or not body.get("ok"):
            raise LvlAgentShopError("recommendations failed", status=status, body=body)
        return body

    def coverage(self) -> dict:
        status, body, _ = self._request("/api/coverage", accept_statuses={200})
        if not body or not body.get("ok"):
            raise LvlAgentShopError("coverage failed", status=status, body=body)
        return body

    def pipelines(self, pipeline_id: Optional[str] = None) -> dict:
        path = (
            f"/api/pipelines?id={urllib.parse.quote(pipeline_id)}"
            if pipeline_id
            else "/api/pipelines"
        )
        status, body, _ = self._request(path, accept_statuses={200, 404})
        if status != 200 or not body or not body.get("ok"):
            raise LvlAgentShopError("pipelines failed", status=status, body=body)
        return body

    def status_history(self, path: str = "/api/pay", *, probe: bool = False, hours: int = 168) -> dict:
        q = f"path={urllib.parse.quote(path)}&hours={hours}"
        if probe:
            q += "&probe=1"
        status, body, _ = self._request(f"/api/status-history?{q}", accept_statuses={200})
        if not body or not body.get("ok"):
            raise LvlAgentShopError("status_history failed", status=status, body=body)
        return body

    def price_history(self, skill_id: Optional[str] = None) -> dict:
        path = (
            f"/api/price-history?skill={urllib.parse.quote(skill_id)}"
            if skill_id
            else "/api/price-history"
        )
        status, body, _ = self._request(path, accept_statuses={200})
        if not body or not body.get("ok"):
            raise LvlAgentShopError("price_history failed", status=status, body=body)
        return body


if __name__ == "__main__":
    s = LvlAgentShop()
    ch = s.challenge("agent-x402-first-buy")
    print("challenge payTo", ch["payTo"], "amount", ch["amountAtomic"])
    print("treasury", s.contracts().get("treasury", {}).get("pay_to"))
    print("pipelines", s.pipelines().get("count"))
