"""GetFixn starter client. Standard library only (Python 3.9+).

    export GETFIXN_KEY=tfx_live_...      # only needed for paid pulls
Free previews work with no key.

Notes learned the hard way:
  * Send a User-Agent. The default Python one is blocked (HTTP 403).
  * A paid pull without a valid key or payment returns 402 with the price, not 401.
  * Free previews are limited to 60 requests per minute per IP (HTTP 429).
"""
import json, os, time, urllib.error, urllib.parse, urllib.request

BASE = "https://data.truefixr.com"
UA = "getfixn-starter/1.0"


def _price_usd(body):
    """x402 v2: accepts[0].amount is atomic USDC (6 decimals). Falls back to the legacy for_ai_agents.price_usd."""
    if not isinstance(body, dict):
        return None
    try:
        return int(body["accepts"][0]["amount"]) / 1_000_000
    except (KeyError, IndexError, TypeError, ValueError):
        pass
    legacy = (body.get("for_ai_agents") or {}).get("price_usd")
    return legacy if isinstance(legacy, (int, float)) else None


class PaymentRequired(Exception):
    """HTTP 402. .price_usd is the price of that call when the server states it."""
    def __init__(self, body):
        self.body = body
        self.price_usd = _price_usd(body)
        cost = f"${self.price_usd:g}" if self.price_usd is not None else "an amount the server did not state"
        super().__init__(f"402 payment required: this call costs {cost}. Add a valid key or fund your balance.")


class ApiError(Exception):
    def __init__(self, status, body):
        self.status, self.body = status, body
        super().__init__(f"HTTP {status}: {body}")


class Client:
    def __init__(self, key=None, base=BASE, min_interval=1.0, cache_ttl=300, retries=3, timeout=90):
        self.key = key or os.environ.get("GETFIXN_KEY", "")
        self.base, self.timeout, self.retries = base, timeout, retries
        self.min_interval = min_interval      # simple rate limit: seconds between calls
        self.cache_ttl, self._cache, self._last = cache_ttl, {}, 0.0

    def _send(self, method, url, body=None):
        wait = self.min_interval - (time.time() - self._last)
        if wait > 0:
            time.sleep(wait)
        headers = {"User-Agent": UA, "Accept": "application/json"}
        if self.key:
            headers["Authorization"] = f"Bearer {self.key}"
        data = None
        if body is not None:
            data = json.dumps(body).encode()
            headers["Content-Type"] = "application/json"
        for attempt in range(self.retries + 1):
            self._last = time.time()
            try:
                req = urllib.request.Request(url, data=data, headers=headers, method=method)
                with urllib.request.urlopen(req, timeout=self.timeout) as r:
                    return json.loads(r.read())
            except urllib.error.HTTPError as e:
                raw = e.read().decode("utf-8", "replace")
                try:
                    parsed = json.loads(raw)
                except ValueError:
                    parsed = raw
                if e.code == 402:
                    raise PaymentRequired(parsed)
                if e.code in (429, 502, 503, 504) and attempt < self.retries:
                    time.sleep(min(2 ** attempt * 5, 60))   # back off, then retry
                    continue
                raise ApiError(e.code, parsed)
            except (urllib.error.URLError, TimeoutError):
                if attempt >= self.retries:
                    raise
                time.sleep(2 ** attempt)

    def get(self, **params):
        """GET /v1/data. Results are cached in memory for cache_ttl seconds."""
        params = {k: ("true" if v is True else v) for k, v in params.items() if v is not None and v is not False}
        url = f"{self.base}/v1/data?{urllib.parse.urlencode(params)}"
        hit = self._cache.get(url)
        if hit and time.time() - hit[0] < self.cache_ttl:
            return hit[1]
        out = self._send("GET", url)
        self._cache[url] = (time.time(), out)
        return out

    def post(self, path, body):
        return self._send("POST", f"{self.base}{path}", body)


def records(resp):
    """First list of objects in a response. Paid pulls return the matching records as a list;
    this finds it without guessing the field name. Check the real shape with one small paid call."""
    if isinstance(resp, list):
        return resp
    for v in resp.values():
        if isinstance(v, list) and v and isinstance(v[0], dict):
            return v
    return []
