import json
import time
import urllib.error
import urllib.request


DEFAULT_API_BASE = "https://iricity-webapp-api.onrender.com"
TERMINAL_STATUSES = {"completed", "failed", "canceled", "timed_out"}
TERMINAL_ORDER_STATUSES = {"completed", "failed", "canceled", "expired", "refunded", "chargeback"}


class IricitySdkError(RuntimeError):
    def __init__(self, message, status=None, payload=None):
        super().__init__(message)
        self.status = status
        self.payload = payload


class IricityClient:
    def __init__(
        self,
        api_key=None,
        api_base=DEFAULT_API_BASE,
        poll_interval_seconds=1.5,
        wait_timeout_seconds=180,
        session_token=None,
        auth_token=None,
    ):
        resolved_token = auth_token or api_key or session_token
        if not resolved_token:
            raise IricitySdkError("Missing auth token. Pass api_key, session_token, or auth_token.")
        self.api_key = resolved_token
        self.api_base = api_base.rstrip("/")
        self.poll_interval_seconds = poll_interval_seconds
        self.wait_timeout_seconds = wait_timeout_seconds

    def _request_json(self, path, method="GET", headers=None, body=None):
        request = urllib.request.Request(f"{self.api_base}{path}", method=method)
        request.add_header("Authorization", f"Bearer {self.api_key}")
        request.add_header("Content-Type", "application/json")
        for key, value in (headers or {}).items():
            request.add_header(key, value)

        payload = None if body is None else json.dumps(body).encode("utf-8")
        try:
            with urllib.request.urlopen(request, data=payload) as response:
                text = response.read().decode("utf-8")
                return json.loads(text) if text else {}
        except urllib.error.HTTPError as exc:
            text = exc.read().decode("utf-8") if exc.fp else ""
            payload = json.loads(text) if text else {}
            message = payload.get("error") if isinstance(payload, dict) else None
            raise IricitySdkError(message or f"Request failed with {exc.code}", exc.code, payload) from exc

    def catalog(self):
        return self._request_json("/v1/internet-payments/catalog")

    def list_orders(self):
        return self._request_json("/v1/payments/paddle/orders")

    def list_subscriptions(self):
        return self._request_json("/v1/payments/paddle/subscriptions")

    def list_api_keys(self):
        return self._request_json("/v1/auth/api-keys")

    def create_api_key(self, label=None, scopes=None, expires_at=None, rate_limit_per_minute=None):
        body = {}
        if label:
            body["label"] = label
        if isinstance(scopes, list):
            body["scopes"] = scopes
        if expires_at:
            body["expiresAt"] = expires_at
        if isinstance(rate_limit_per_minute, int):
            body["rateLimitPerMinute"] = rate_limit_per_minute
        return self._request_json("/v1/auth/api-keys", method="POST", body=body)

    def create_pack_checkout_intent(self, package_id):
        if not package_id:
            raise IricitySdkError("Missing package_id.")
        return self._request_json(
            "/v1/payments/paddle/checkout-intents",
            method="POST",
            body={"packageId": package_id},
        )

    def create_subscription_checkout_intent(self, subscription_plan_id):
        if not subscription_plan_id:
            raise IricitySdkError("Missing subscription_plan_id.")
        return self._request_json(
            "/v1/payments/paddle/subscription-checkout-intents",
            method="POST",
            body={"subscriptionPlanId": subscription_plan_id},
        )

    def get_order(self, order_id):
        if not order_id:
            raise IricitySdkError("Missing order_id.")
        data = self.list_orders()
        orders = data.get("orders") if isinstance(data, dict) else None
        if isinstance(orders, list):
            for order in orders:
                if isinstance(order, dict) and order.get("id") == order_id:
                    return order
        raise IricitySdkError(f"Order {order_id} not found.", 404, data)

    def wait_for_order(self, order_id, timeout_seconds=None, poll_interval_seconds=None):
        timeout_seconds = timeout_seconds or self.wait_timeout_seconds
        poll_interval_seconds = poll_interval_seconds or self.poll_interval_seconds
        started = time.time()

        while time.time() - started <= timeout_seconds:
            order = self.get_order(order_id)
            if order and order.get("status") in TERMINAL_ORDER_STATUSES:
                return order
            time.sleep(poll_interval_seconds)

        raise IricitySdkError(f"Timed out waiting for order {order_id}.")

    def quote(self, feature_id, input_payload):
        return self._request_json(
            "/v1/internet-payments/quotes/execution",
            method="POST",
            body={"featureId": feature_id, "input": input_payload},
        )

    def execute(self, feature_id, input_payload, idempotency_key=None, allow_fallback=True, receipt_token=None):
        receipt = receipt_token or self.quote(feature_id, input_payload).get("paymentRequired", {}).get("receiptToken")
        if not receipt:
            raise IricitySdkError("Quote did not return a payment receipt.")

        return self._request_json(
            "/v1/internet-payments/execute",
            method="POST",
            headers={"x-iricity-payment-receipt": receipt},
            body={
                "featureId": feature_id,
                "input": input_payload,
                "idempotencyKey": idempotency_key or f"sdk-py-{int(time.time())}",
                "allowFallback": allow_fallback,
            },
        )

    def get_job(self, job_id):
        data = self._request_json(f"/v1/execution/jobs/{job_id}")
        return data.get("job")

    def wait_for_job(self, job_id, timeout_seconds=None, poll_interval_seconds=None):
        timeout_seconds = timeout_seconds or self.wait_timeout_seconds
        poll_interval_seconds = poll_interval_seconds or self.poll_interval_seconds
        started = time.time()

        while time.time() - started <= timeout_seconds:
            job = self.get_job(job_id)
            if job and job.get("status") in TERMINAL_STATUSES:
                return job
            time.sleep(poll_interval_seconds)

        raise IricitySdkError(f"Timed out waiting for job {job_id}.")

    def run(self, feature_id, input_payload, idempotency_key=None, wait=False, allow_fallback=True):
        quote = self.quote(feature_id, input_payload)
        execute = self.execute(
            feature_id,
            input_payload,
            idempotency_key=idempotency_key,
            allow_fallback=allow_fallback,
            receipt_token=quote.get("paymentRequired", {}).get("receiptToken"),
        )
        job_id = (execute.get("settled") or {}).get("job", {}).get("id") or (execute.get("execute") or {}).get("id")
        settled = (
            {"job": self.wait_for_job(job_id), "initial": execute.get("settled")}
            if wait and job_id
            else execute.get("settled")
        )
        return {"quote": quote.get("quote"), "execute": execute.get("execute"), "settled": settled}
