"""Dependency-free Inbini SDK. Source preview; Python 3.10+."""
import json
import time
from urllib.request import Request, urlopen
from urllib.parse import urlencode, quote, urlparse
from urllib.error import HTTPError

class InbiniError(Exception):
    def __init__(self, message, status=0, code="API_ERROR", retry_after=1):
        super().__init__(message)
        self.status, self.code, self.retry_after = status, code, retry_after

class Inbini:
    def __init__(self, *, base_url, workspace_token=None):
        if urlparse(base_url).scheme not in ("http", "https"):
            raise ValueError("Use an HTTP(S) API base URL")
        self.base_url = base_url.rstrip("/")
        self.workspace_token = workspace_token
        self.inboxes = _Inboxes(self)
        self.domains = _Domains(self)

    def create_workspace(self):
        return self._request("/workspaces", "POST", body={})

    def _request(self, path, method="GET", token=None, body=None, timeout=10):
        headers = {"Authorization": "Bearer " + token} if token else {}
        data = None
        if body is not None:
            headers["Content-Type"] = "application/json"
            data = json.dumps(body).encode()
        request = Request(self.base_url + path, data=data, headers=headers, method=method)
        try:
            with urlopen(request, timeout=max(.001, timeout)) as response:
                return None if response.status == 204 else json.load(response)["data"]
        except HTTPError as error:
            try:
                details = json.load(error).get("error", {})
            except (ValueError, KeyError):
                details = {}
            raise InbiniError(details.get("message", "API request failed"), error.code,
                                details.get("code", "API_ERROR"),
                                float(error.headers.get("Retry-After", 1))) from error

class _Domains:
    def __init__(self, client):
        self.client = client
    def _call(self, suffix="", method="GET", body=None):
        return self.client._request("/domains" + suffix, method, self.client.workspace_token, body)
    def list(self):
        return self._call()
    def add(self, hostname):
        return self._call(method="POST", body={"hostname": hostname})
    def get(self, id):
        return self._call("/" + quote(id, safe=""))
    def verify(self, id):
        return self._call("/" + quote(id, safe="") + "/verify", "POST", {})
    def receiver_key(self, id):
        return self._call("/" + quote(id, safe="") + "/receiver-key", "POST", {})
    def remove(self, id):
        return self._call("/" + quote(id, safe=""), "DELETE")

class _Inboxes:
    def __init__(self, client):
        self.client = client
    def create(self, *, expires_in="1h", domain_id=None):
        body = {"expiresIn": expires_in}
        if domain_id is not None:
            body["domainId"] = domain_id
        data = self.client._request("/inboxes", "POST", token=self.client.workspace_token if domain_id else None, body=body)
        return Inbox(self.client, data, data.pop("token"))
    def resume(self, *, id, token):
        data = self.client._request("/inboxes/" + quote(id, safe=""), token=token)
        return Inbox(self.client, data, token)

class Inbox:
    def __init__(self, client, data, token):
        self._client, self.data, self.token = client, data, token
        self.id, self.address, self.expires_at = data["id"], data["address"], data["expiresAt"]
        self._path = "/inboxes/" + quote(self.id, safe="")
    def messages(self, *, sender=None, subject=None, after=None, _timeout=10):
        filters = {k:v for k,v in {"from":sender,"subject":subject,"after":after}.items() if v}
        return self._client._request(self._path + "/messages?" + urlencode(filters), token=self.token, timeout=_timeout)
    def get_message(self, id):
        return self._client._request(self._path + "/messages/" + quote(id, safe=""), token=self.token)
    def delete_message(self, id):
        return self._client._request(self._path + "/messages/" + quote(id, safe=""), "DELETE", self.token)
    def delete(self):
        return self._client._request(self._path, "DELETE", self.token)
    def sample(self):
        return self._client._request(self._path + "/sample", "POST", self.token, {})
    def _wait(self, select, *, timeout=60, **filters):
        if not isinstance(timeout, (int, float)) or not 0 < timeout <= 300:
            raise ValueError("timeout must be between 0 and 300 seconds")
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            delay = 1
            try:
                for message in self.messages(**filters, _timeout=min(10, max(.001, deadline-time.monotonic()))):
                    result = select(message)
                    if result is not None:
                        return result
            except InbiniError as error:
                if error.status != 429:
                    raise
                delay = max(1, error.retry_after)
            except TimeoutError:
                if time.monotonic() < deadline:
                    raise
            time.sleep(min(delay, max(0, deadline-time.monotonic())))
        raise InbiniError("No matching message arrived before the timeout.", 408, "WAIT_TIMEOUT")
    def wait_for_message(self, **options):
        return self._wait(lambda m: m, **options)
    def wait_for_code(self, **options):
        return self._wait(lambda m: m.get("code"), **options)
    def wait_for_link(self, **options):
        return self._wait(lambda m: next(iter(m.get("links", [])), None), **options)
