"""xMap Flows client for Python 3.8 or newer. MIT licence. Standard library only.
Keep the access key on your server.

    from xmapflows import XMapFlows
    xf = XMapFlows(os.environ["XMAP_FLOWS_KEY"])
    flows = xf.flows()
    run = xf.run_flow(flows[0]["id"], send=False)
    print(run["status"], run["count"], run["items"])

Reference: https://flows.xmap.ai/api-docs/
"""
import hashlib
import hmac
import json
import re
import time
import urllib.error
import urllib.parse
import urllib.request

VERSION = "1.0.0"


class XMapFlowsError(Exception):
    def __init__(self, message, status=0, code=None, body=None):
        super().__init__(message)
        self.status, self.code, self.body = status, code, body


class XMapFlows:
    def __init__(self, key, base_url="https://flows.xmap.ai/api", retries=2, timeout=300):
        if not re.match(r"^xfk_[\w-]{20,}$", str(key or "")):
            raise XMapFlowsError("Pass an xMap Flows access key (xfk_...). Make one in Settings, AI assistants, or ask an admin for an organization key.", 0, "no_key")
        self.key, self.base_url, self.retries, self.timeout = key, base_url.rstrip("/"), retries, timeout

    def request(self, method, path, body=None):
        """One request; too many requests (429) and passing server errors are tried again after a pause."""
        data = None if body is None else json.dumps(body).encode("utf-8")
        for attempt in range(self.retries + 1):
            req = urllib.request.Request(self.base_url + path, data=data, method=method, headers={
                "Authorization": "Bearer " + self.key, "Content-Type": "application/json", "User-Agent": "xmapflows-python/" + VERSION})
            try:
                with urllib.request.urlopen(req, timeout=self.timeout) as r:
                    text = r.read().decode("utf-8")
                    return json.loads(text) if text else None
            except urllib.error.HTTPError as e:
                text = e.read().decode("utf-8", "replace")
                try:
                    payload = json.loads(text)
                except ValueError:
                    payload = {"error": text}
                if (e.code == 429 or e.code >= 500) and attempt < self.retries:
                    time.sleep(min(30, float(e.headers.get("Retry-After") or 2 ** attempt)))
                    continue
                raise XMapFlowsError(payload.get("error") or "xMap Flows answered %d" % e.code, e.code, payload.get("code"), payload)
            except urllib.error.URLError as e:
                if attempt < self.retries:
                    time.sleep(2 ** attempt)
                    continue
                raise XMapFlowsError("Could not reach xMap Flows: %s" % e.reason, 0, "network")

    def me(self):
        return self.request("GET", "/v1/me")

    def flows(self, organization_id=None):
        return self.request("GET", "/v1/flows" + ("?organization_id=" + urllib.parse.quote(organization_id) if organization_id else ""))

    def run_flow(self, flow_id, send=False, input=None):
        """send=True runs it for real (live data, and its emails and messages are sent); input={"items": [...]} for a Webhook flow."""
        body = {"send": bool(send)}
        if input is not None:
            body["input"] = input
        return self.request("POST", "/v1/flows/%s/run" % urllib.parse.quote(flow_id), body)

    def run(self, run_id):
        return self.request("GET", "/v1/runs/%s" % urllib.parse.quote(run_id))

    def data_products(self):
        return self.request("GET", "/v1/data")

    def data(self, product, body=None):
        return self.request("POST", "/v1/data/%s" % urllib.parse.quote(product), body or {})

    @staticmethod
    def call_webhook(url, payload, wait=True, idempotency_key=None):
        """A live flow's webhook address (from its Webhook block): no key needed, the address is the secret."""
        headers = {"Content-Type": "application/json"}
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key
        full = url if wait else url + ("&" if "?" in url else "?") + "wait=0"
        req = urllib.request.Request(full, data=json.dumps(payload).encode("utf-8"), method="POST", headers=headers)
        try:
            with urllib.request.urlopen(req, timeout=300) as r:
                return json.loads(r.read().decode("utf-8") or "null")
        except urllib.error.HTTPError as e:
            raise XMapFlowsError("The webhook answered %d" % e.code, e.code)

    @staticmethod
    def verify_signature(raw_body, header, secret):
        """Checks the X-XFlows-Signature header of an alert webhook xMap Flows sent you."""
        if isinstance(raw_body, str):
            raw_body = raw_body.encode("utf-8")
        want = "sha256=" + hmac.new(str(secret).encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
        return hmac.compare_digest(want, str(header or ""))
