"""APP v0 client (Agent Payment Protocol, Okidooki ecosystem). Python 3.9+, standard library only.

    from app_v0 import AppV0Client
    mif = AppV0Client("https://europe-west1-make-it-fun-app.cloudfunctions.net/a2aGateway", os.environ["MAKEITFUN_KEY"])
    card = mif.card()
    sellers = mif.call("discover", {"productType": "tshirt"})
    quote = mif.call("quote", {"userRef": user_ref, "designId": design_id, "variantId": 4012, "quantity": 2})
    order = mif.call("order", {"userRef": user_ref, "quoteId": quote["quoteId"]}, idempotency_key="my-order-1")

Signing: Authorization: Bearer <key>, X-Timestamp: <unix seconds>,
X-Signature: hex(HMAC-SHA256(key, f"{timestamp}.{raw_body}")). The raw body must be
byte-identical to what is signed, so the client serialises once (compact JSON).
"""
import hashlib
import hmac
import json
import time
import urllib.error
import urllib.request
from typing import Any, Dict, Optional


class AppV0Error(Exception):
    def __init__(self, status: int, code: str, message: str, retryable: bool = False, request_id: Optional[str] = None):
        super().__init__(message)
        self.status, self.code, self.retryable, self.request_id = status, code, retryable, request_id


class AppV0Client:
    def __init__(self, gateway: str, key: str, timeout: float = 30.0, retries: int = 2):
        if not gateway or not key:
            raise ValueError("gateway and key are required")
        self.gateway = gateway.rstrip("/")
        self.key = key
        self.timeout = timeout
        self.retries = retries
        self.mode = "test" if "_test_" in key else "live"

    def card(self) -> Dict[str, Any]:
        for url in (f"{self.gateway}/card", f"{self.gateway}/a2a/v0/card", f"{self.gateway}/.well-known/agent.json"):
            try:
                with urllib.request.urlopen(urllib.request.Request(url), timeout=self.timeout) as r:
                    j = json.loads(r.read().decode("utf-8"))
                    if j.get("protocol") == "okidooki-app" or url.endswith("agent.json"):
                        return j
            except Exception:
                continue
        raise AppV0Error(404, "not_found", f"No agent card at {self.gateway}")

    def sign(self, timestamp: str, raw_body: str) -> str:
        return hmac.new(self.key.encode("utf-8"), f"{timestamp}.{raw_body}".encode("utf-8"), hashlib.sha256).hexdigest()

    def call(self, route: str, body: Optional[Dict[str, Any]] = None, idempotency_key: Optional[str] = None) -> Any:
        raw_body = json.dumps(body or {}, separators=(",", ":"), ensure_ascii=False)
        last: Optional[AppV0Error] = None
        for attempt in range(self.retries + 1):
            timestamp = str(int(time.time()))
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.key}",
                "X-Timestamp": timestamp,
                "X-Signature": self.sign(timestamp, raw_body),
            }
            if idempotency_key:
                headers["Idempotency-Key"] = idempotency_key
            req = urllib.request.Request(f"{self.gateway}/a2a/v0/{route}", data=raw_body.encode("utf-8"), headers=headers, method="POST")
            try:
                with urllib.request.urlopen(req, timeout=self.timeout) as r:
                    text = r.read().decode("utf-8")
                    return json.loads(text) if text else None
            except urllib.error.HTTPError as e:
                text = e.read().decode("utf-8", "replace")
                try:
                    err = (json.loads(text) or {}).get("error") or {}
                except Exception:
                    err = {}
                retryable = e.code == 429 or e.code >= 500 or err.get("retryable") is True
                last = AppV0Error(e.code, err.get("code") or f"http_{e.code}", err.get("message") or text[:200], retryable, e.headers.get("x-request-id"))
                if retryable and attempt < self.retries and not (route == "order" and not idempotency_key):
                    time.sleep(2.0 if e.code == 429 else 0.6 * (attempt + 1))
                    continue
                raise last
            except urllib.error.URLError as e:
                last = AppV0Error(0, "network", str(e.reason), True)
                if attempt < self.retries:
                    time.sleep(0.4 * (attempt + 1))
                    continue
                raise last
        raise last  # pragma: no cover

    # convenience wrappers
    def discover(self, body): return self.call("discover", body)
    def quote(self, body): return self.call("quote", body)
    def order(self, body, idempotency_key=None): return self.call("order", body, idempotency_key)
    def orders(self, body): return self.call("orders", body)
    def status(self, body): return self.call("status", body)
    def cancel(self, body): return self.call("cancel", body)
    def link_start(self, body): return self.call("link/start", body)
    def link_status(self, body): return self.call("link/status", body)
    # Universal "Connect with Okidooki" (Okidooki gateway only)
    def connect_start(self, body): return self.call("connect/start", body)
    def connect_status(self, body): return self.call("connect/status", body)

    def wait_connect(self, connect_id: str, every: float = 10.0, max_seconds: float = 900.0) -> Dict[str, Any]:
        started = time.time()
        while True:
            c = self.connect_status({"connectId": connect_id})
            if c.get("status") != "pending" or time.time() - started > max_seconds:
                return c
            time.sleep(every)
    def preview(self, body): return self.call("preview", body)
    def preview_status(self, body): return self.call("preview/status", body)

    def wait_preview(self, job_id: str, every: float = 10.0, max_seconds: float = 300.0) -> Dict[str, Any]:
        started = time.time()
        while time.time() - started < max_seconds:
            s = self.preview_status({"jobId": job_id})
            if s.get("status") in ("done", "failed"):
                return s
            time.sleep(every)
        raise AppV0Error(504, "timeout", "preview did not finish in time", True)


def registry() -> Dict[str, Any]:
    with urllib.request.urlopen("https://okidooki.app/.well-known/app-v0.json", timeout=15) as r:
        return json.loads(r.read().decode("utf-8"))


def verify_webhook(secret: str, headers: Dict[str, str], raw_body: str, tolerance_seconds: int = 300) -> Dict[str, Any]:
    """Verify a partner webhook from the Okidooki hub (X-APPV0-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, f"{t}.{raw_body}")>)."""
    sig = next((v for k, v in headers.items() if k.lower() == "x-appv0-signature"), "")
    parts = dict(p.split("=", 1) for p in sig.split(",") if "=" in p)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        raise ValueError("missing signature")
    if abs(time.time() - int(t)) > tolerance_seconds:
        raise ValueError("stale signature")
    expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, v1):
        raise ValueError("bad signature")
    return json.loads(raw_body)
