"""
Payora merchant SDK for Python — zero dependencies (standard library only).
Mirrors the PHP/Node SDKs: HMAC-SHA256 signed requests, and (critically) webhook verification.

    from payora import Payora
    payora = Payora("https://api.payora.money", "pk_...", "sk_...")
    inv = payora.create_invoice({"order_id": "ORDER-42", "mode": "fiat", "amount": "9.99"})
    # redirect the buyer to inv["pay_url"] (the hosted checkout)

    # in your webhook endpoint (needs the RAW request body bytes, not a parsed dict):
    event = payora.verify_webhook(raw_body, request_headers)
    if event and event["status"] == "paid":
        credit_order_idempotently(event["order_id"])
"""
import hashlib
import hmac
import json
import time
import urllib.error
import urllib.parse
import urllib.request


class PayoraError(Exception):
    pass


class Payora:
    def __init__(self, api_base, api_key, api_secret, skew=300):
        self.api_base = api_base.rstrip("/")
        self.api_key = api_key
        self.api_secret = api_secret
        self.skew = skew

    # ---- API methods -------------------------------------------------------
    def create_invoice(self, body, idempotency_key=None):
        return self._request("POST", "/v1/invoice", body, idempotency_key)

    def get_invoice(self, invoice_id):
        return self._request("GET", "/v1/invoice/" + urllib.parse.quote(str(invoice_id)), None)

    def list_invoices(self, params=None):
        return self._request("GET", "/v1/invoices" + self._qs(params), None)

    def cancel_invoice(self, invoice_id):
        return self._request("POST", "/v1/invoice/" + urllib.parse.quote(str(invoice_id)) + "/cancel", None)

    def get_balance(self):
        return self._request("GET", "/v1/balance", None)

    def create_payout(self, body, idempotency_key=None):
        return self._request("POST", "/v1/payout", body, idempotency_key)

    def get_payout(self, payout_id):
        return self._request("GET", "/v1/payout/" + urllib.parse.quote(str(payout_id)), None)

    def list_payouts(self, params=None):
        return self._request("GET", "/v1/payout" + self._qs(params), None)

    def cancel_payout(self, payout_id):
        return self._request("POST", "/v1/payout/" + urllib.parse.quote(str(payout_id)) + "/cancel", None)

    def verify_webhook(self, raw_body, headers):
        """Return the decoded event dict, or None if signature/timestamp/body are invalid.
        NEVER credit an order without a non-None return. `raw_body` must be the exact bytes/str."""
        raw = raw_body.decode("utf-8") if isinstance(raw_body, (bytes, bytearray)) else str(raw_body)
        h = {str(k).lower(): str(v) for k, v in (headers or {}).items()}
        ts = h.get("x-payora-timestamp", "")
        sig = h.get("x-payora-signature", "").strip().lower()
        if not ts or not sig or not ts.isdigit():
            return None
        if abs(int(time.time()) - int(ts)) > self.skew:
            return None  # stale / replayed
        expected = hmac.new(self.api_secret.encode("utf-8"), (ts + "." + raw).encode("utf-8"), hashlib.sha256).hexdigest()
        if not hmac.compare_digest(expected, sig):
            return None  # forged
        try:
            data = json.loads(raw)
        except ValueError:
            return None
        return data if isinstance(data, dict) else None

    # ---- internals ---------------------------------------------------------
    def _qs(self, params):
        params = {k: v for k, v in (params or {}).items() if v is not None}
        return ("?" + urllib.parse.urlencode(params)) if params else ""

    def _request(self, method, path, body, idempotency_key=None):
        raw = json.dumps(body, separators=(",", ":")) if body is not None else ""
        ts = str(int(time.time()))
        sig = hmac.new(self.api_secret.encode("utf-8"), (ts + "." + raw).encode("utf-8"), hashlib.sha256).hexdigest()
        headers = {
            "X-Payora-Key": self.api_key,
            "X-Payora-Timestamp": ts,
            "X-Payora-Signature": sig,
            "Accept": "application/json",
        }
        if raw != "":
            headers["Content-Type"] = "application/json"
        if idempotency_key is not None:
            headers["Idempotency-Key"] = idempotency_key

        data = raw.encode("utf-8") if raw != "" else None
        req = urllib.request.Request(self.api_base + path, data=data, headers=headers, method=method)
        try:
            with urllib.request.urlopen(req, timeout=20) as resp:
                text = resp.read().decode("utf-8")
                decoded = json.loads(text) if text else {}
                return decoded if isinstance(decoded, dict) else {}
        except urllib.error.HTTPError as e:
            text = e.read().decode("utf-8", "replace")
            try:
                decoded = json.loads(text)
                msg = decoded.get("error", {}).get("message", "HTTP %d" % e.code)
            except ValueError:
                msg = "HTTP %d" % e.code
            raise PayoraError("Payora API error (%d): %s" % (e.code, msg))
        except urllib.error.URLError as e:
            raise PayoraError("Payora transport error: %s" % e.reason)
