"""
ListNRide Partner API - official Python client (single file, standard library only, Python 3.8+).
Docs: https://developers.listnride.com

    from listnride import ListNRide, verify_webhook
    lnr = ListNRide(api_key=os.environ["LNR_API_KEY"])
    rides = lnr.get("/rides", lat=52.52, lng=13.40, category="e-bike")
    for ride in lnr.all("/rides", lat=52.52, lng=13.40):
        ...
    session = lnr.create_checkout_session({...})   # adds an Idempotency-Key for you
    event = verify_webhook(raw_body, request.headers["LNR-Signature"], secret)

Retries 429 and 5xx automatically (honouring Retry-After). POST requests are
retried only with an Idempotency-Key.
"""
import hashlib
import hmac
import json
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid

VERSION = "1.0.0"
DEFAULT_BASE = "https://api.listnride.com/v1"


class ListNRideError(Exception):
    def __init__(self, status, body):
        err = (body or {}).get("error", {}) if isinstance(body, dict) else {}
        super().__init__(err.get("message") or "HTTP %s" % status)
        self.status = status
        self.type = err.get("type")
        self.code = err.get("code")
        self.param = err.get("param")
        self.request_id = err.get("request_id")


class WebhookSignatureError(Exception):
    pass


class ListNRide:
    def __init__(self, api_key, base_url=DEFAULT_BASE, max_retries=2, timeout=30):
        if not str(api_key or "").startswith(("lnr_test_", "lnr_live_")):
            raise ValueError("api_key must start with lnr_test_ or lnr_live_")
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.max_retries = max_retries
        self.timeout = timeout

    # -- convenience -------------------------------------------------------
    def get(self, path, **query):
        return self.request("GET", path, query=query)

    def post(self, path, body=None, idempotency_key=None):
        return self.request("POST", path, body=body or {}, idempotency_key=idempotency_key)

    def create_quote(self, body):
        return self.post("/quotes", body)

    def create_checkout_session(self, body, idempotency_key=None):
        return self.post("/checkout/sessions", body, idempotency_key or str(uuid.uuid4()))

    def cancel_booking(self, booking_id, body=None, idempotency_key=None):
        return self.post("/bookings/%s/cancel" % urllib.parse.quote(booking_id, safe=""), body or {}, idempotency_key or str(uuid.uuid4()))

    def all(self, path, **query):
        """Iterates over all items of a list endpoint, following next_cursor."""
        cursor = None
        while True:
            page = self.get(path, **dict(query, **({"cursor": cursor} if cursor else {})))
            for item in page.get("data", []):
                yield item
            if not page.get("has_more"):
                return
            cursor = page.get("next_cursor")

    # -- low level ---------------------------------------------------------
    def request(self, method, path, query=None, body=None, idempotency_key=None):
        q = {k: (",".join(map(str, v)) if isinstance(v, (list, tuple)) else v)
             for k, v in (query or {}).items() if v is not None and v != ""}
        url = self.base_url + path + ("?" + urllib.parse.urlencode(q) if q else "")
        headers = {
            "Authorization": "Bearer " + self.api_key,
            "Accept": "application/json",
            "User-Agent": "listnride-python/" + VERSION,
        }
        data = None
        if body is not None:
            headers["Content-Type"] = "application/json"
            data = json.dumps(body).encode("utf-8")
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key
        retryable = method == "GET" or bool(idempotency_key)

        attempt = 0
        while True:
            req = urllib.request.Request(url, data=data, headers=headers, method=method)
            try:
                with urllib.request.urlopen(req, timeout=self.timeout) as res:
                    raw = res.read().decode("utf-8")
                    return json.loads(raw) if raw else None
            except urllib.error.HTTPError as e:
                raw = e.read().decode("utf-8", "replace")
                try:
                    parsed = json.loads(raw) if raw else None
                except ValueError:
                    parsed = {"error": {"message": raw[:200]}}
                if (e.code == 429 or e.code >= 500) and retryable and attempt < self.max_retries:
                    ra = e.headers.get("Retry-After")
                    time.sleep(min(int(ra), 60) if ra and ra.isdigit() and int(ra) > 0 else 0.5 * 2 ** attempt)
                    attempt += 1
                    continue
                raise ListNRideError(e.code, parsed)
            except urllib.error.URLError:
                if retryable and attempt < self.max_retries:
                    time.sleep(0.5 * 2 ** attempt)
                    attempt += 1
                    continue
                raise


def verify_webhook(raw_body, signature_header, secret, tolerance_sec=300):
    """Verifies a webhook and returns the parsed event. raw_body: bytes or str exactly as received."""
    parts = dict(p.strip().split("=", 1) for p in str(signature_header or "").split(",") if "=" in p)
    try:
        t = int(parts.get("t", "0"))
    except ValueError:
        t = 0
    if not t or not parts.get("v1"):
        raise WebhookSignatureError("Missing t or v1 in LNR-Signature.")
    if abs(time.time() - t) > tolerance_sec:
        raise WebhookSignatureError("Timestamp outside the tolerance.")
    body = raw_body.decode("utf-8") if isinstance(raw_body, (bytes, bytearray)) else str(raw_body)
    expected = hmac.new(secret.encode("utf-8"), ("%d.%s" % (t, body)).encode("utf-8"), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, parts["v1"]):
        raise WebhookSignatureError("Signature does not match.")
    return json.loads(body)
