"""SMScash API 0.22.0. Python 3.9+; standard library, no automatic retries."""

import json
import re
import urllib.error
import urllib.parse
import urllib.request


class SmScashError(Exception):
    def __init__(self, error, status, request_id=None):
        super().__init__(error.get("message", "SMScash API request failed"))
        self.code = error.get("code", "HTTP_ERROR")
        self.status = status
        self.request_id = error.get("request_id") or request_id
        self.hint = error.get("hint")
        self.details = error.get("details")
        self.retryable = error.get("retryable") is True
        self.retry_after_seconds = error.get("retry_after_seconds")


class _NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


def _positive(value):
    return type(value) is int and value > 0


def _validate_operator(parameters):
    if "provider_id" in parameters:
        raise ValueError("Use operator instead of provider_id.")
    if "operator" not in parameters:
        return
    value = parameters["operator"]
    if type(value) is int and 0 <= value <= 2147483647:
        return
    if isinstance(value, str):
        value = value.strip()
        if value.lower() in ("", "any", "0"):
            return
        ids = [part.strip() for part in value.split(",")]
        if len(value) <= 2200 and len(ids) <= 100 and all(re.fullmatch(r"[0-9]+", part) and 0 < int(part) <= 2147483647 for part in ids):
            return
    raise ValueError("operator must be a public provider ID or up to 100 comma-separated IDs, or any/0 for random selection.")


def _operation_key(value):
    if not isinstance(value, str) or not re.fullmatch(r"[\x21-\x7e]{8,128}", value):
        raise ValueError("Pass a unique Idempotency-Key with 8–128 ASCII characters; keep it for retries of the same operation.")
    return value


class SmScash:
    def __init__(self, api_key, base_url="https://smscash.org", timeout=15):
        if not isinstance(api_key, str) or not api_key or re.search(r"\s", api_key):
            raise ValueError("A nonempty API key is required.")
        url = urllib.parse.urlsplit(base_url)
        if url.scheme != "https" or not url.hostname or url.username or url.password or url.query or url.fragment or url.path not in ("", "/"):
            raise ValueError("base_url must be an HTTPS origin.")
        if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or timeout <= 0:
            raise ValueError("timeout must be positive.")
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._opener = urllib.request.build_opener(_NoRedirect)

    def _request(self, path, method="GET", query=None, body=None, request_key=None):
        url = self._base_url + path
        if query:
            url += "?" + urllib.parse.urlencode({key: value for key, value in query.items() if value is not None and value != ""})
        headers = {"Authorization": "Bearer " + self._api_key, "User-Agent": "SMScash-Python/0.22.0"}
        payload = None
        if body is not None:
            headers["Content-Type"] = "application/json"
            payload = json.dumps(body, ensure_ascii=False).encode("utf-8")
        if request_key is not None:
            headers["Idempotency-Key"] = _operation_key(request_key)
        request = urllib.request.Request(url, data=payload, headers=headers, method=method)
        try:
            with self._opener.open(request, timeout=self._timeout) as response:
                try:
                    result = json.load(response)
                    if not isinstance(result, dict):
                        raise ValueError("Expected an object")
                    return result
                except (ValueError, UnicodeDecodeError):
                    raise SmScashError({"code": "INVALID_RESPONSE", "message": "Expected JSON. Check the operation status before another write."}, response.status, response.headers.get("X-Request-ID")) from None
        except urllib.error.HTTPError as failure:
            with failure:
                try:
                    result = json.load(failure)
                    if not isinstance(result, dict):
                        raise ValueError("Expected an object")
                except (ValueError, UnicodeDecodeError):
                    result = {"error": {"code": "INVALID_RESPONSE", "message": "Expected JSON. Check the operation status before another write."}}
            raise SmScashError(result.get("error", {}), failure.code, failure.headers.get("X-Request-ID")) from None

    def me(self):
        return self._request("/api/v1/me")

    def balance(self):
        return self._request("/api/v1/summary")

    def hold_balance(self):
        return self._request("/api/v1/balance")["held_balance"]

    def catalog(self, **filters):
        _validate_operator(filters)
        return self._request("/api/v1/catalog", query=filters)

    def create_number(self, parameters, request_key):
        _operation_key(request_key)
        if not isinstance(parameters, dict):
            raise ValueError("Pass service + country, or offer_id.")
        _validate_operator(parameters)
        if "offer_id" in parameters:
            if not _positive(parameters["offer_id"]) or "service" in parameters or "country" in parameters:
                raise ValueError("Choose offer_id or service + country, not both.")
        elif not isinstance(parameters.get("service"), str) or not parameters["service"] or not _positive(parameters.get("country")):
            raise ValueError("service must be a catalog code; country must be a positive integer.")
        return self._request("/api/v1/activations", method="POST", body=parameters, request_key=request_key)

    def activation(self, session_id):
        if not _positive(session_id):
            raise ValueError("Session ID must be a positive integer.")
        return self._request("/api/v1/activations/" + str(session_id))

    def sms(self, session_id, after_id=0, limit=100):
        if not _positive(session_id) or type(after_id) is not int or after_id < 0 or not _positive(limit) or limit > 200:
            raise ValueError("Use a positive session ID, after_id >= 0 and limit 1–200.")
        return self._request("/api/v1/activations/" + str(session_id) + "/sms", query={"after_id": after_id, "limit": limit})

    def batch(self, ids):
        if not isinstance(ids, (list, tuple)) or not ids or len(ids) > 100 or any(not _positive(value) for value in ids):
            raise ValueError("Pass 1–100 positive session IDs.")
        return self._request("/api/v1/activations/batch", query={"ids": ",".join(map(str, ids))})

    def set_status(self, session_id, status, request_key=None):
        if not _positive(session_id) or status not in ("retry", "finish", "cancel"):
            raise ValueError("Use a positive session ID and retry, finish or cancel.")
        if status == "retry":
            _operation_key(request_key)
        return self._request("/api/v1/activations/" + str(session_id) + "/" + status, method="POST", request_key=request_key)


# Poll no more often than every 20 seconds; keep codes as strings and a cursor per session.
# Never automatically retry ALLOCATION_UNCERTAIN or any write after a transport error.
