"""ChoiceProof Cloud v1, Python 3.10+, standard library only.
Import ChoiceProofCloud or run: python cloud-client.py materials.json.
Query lookup() after an interrupted request; reuse the original idempotency key.
"""
import json
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request


class CloudAPIError(Exception):
    def __init__(self, status, body):
        error = body.get("error", {})
        super().__init__(error.get("message", "Cloud request failed"))
        self.status, self.code = status, error.get("code", "INVALID_RESPONSE")
        self.request_id = body.get("requestId")
        self.retry_at, self.fields = error.get("retryAt"), error.get("fields")


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


class ChoiceProofCloud:
    def __init__(self, api_key, project_id, url="https://choiceproof.dev"):
        parsed = urllib.parse.urlsplit(url)
        if (parsed.username or parsed.password or parsed.query or parsed.fragment
                or parsed.path not in ("", "/") or not (parsed.scheme == "https"
                or (parsed.scheme == "http" and parsed.hostname in ("localhost", "127.0.0.1")))):
            raise ValueError("Use a Cloud HTTPS origin or localhost.")
        if not re.fullmatch(r"cp_live_[A-Za-z0-9_-]{43}", api_key):
            raise ValueError("Set a valid project API key.")
        self.url, self.project_id, self._key = url.rstrip("/"), project_id, api_key
        self._opener = urllib.request.build_opener(_NoRedirect)

    def request(self, path, body=None, idempotency_key=None):
        headers = {"Content-Type": "application/json", "Authorization": "Bearer " + self._key}
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key
        request = urllib.request.Request(self.url + path, data=None if body is None else json.dumps(body).encode(), headers=headers)
        try:
            with self._opener.open(request, timeout=30) as response:
                return json.loads(response.read(1048577))
        except urllib.error.HTTPError as error:
            try:
                data = json.loads(error.read(1048577))
            except (ValueError, UnicodeDecodeError):
                data = {"error": {"code": "INVALID_RESPONSE", "message": "Unreadable Cloud response. Query the original request before retrying."}}
            raise CloudAPIError(error.code, data) from None

    def connection(self):
        return self.request("/api/v1/connection?" + urllib.parse.urlencode({"projectId": self.project_id}))

    def acceptance(self, connector_id, task_id=None, commit_sha=None):
        return self.request("/api/v1/acceptance?" + urllib.parse.urlencode({"projectId": self.project_id, "connectorId": connector_id, **({"taskId": task_id} if task_id else {}), **({"commitSha": commit_sha} if commit_sha else {})}))

    def acceptance_export(self, job_id):
        return self.request("/api/v1/acceptance/export?" + urllib.parse.urlencode({"projectId": self.project_id, "jobId": job_id}))

    def result_event(self, task_id, connector_id=None):
        return self.request("/api/v1/result-events?" + urllib.parse.urlencode({"projectId": self.project_id, "taskId": task_id, **({"connectorId": connector_id} if connector_id else {})}))

    def authorize_task(self, template_id, task_id, target, **binding):
        return self.request("/api/v1/tasks", {"projectId": self.project_id, "templateId": template_id, "taskId": task_id, "target": target, **binding})

    def verify(self, profile_id, observations, idempotency_key):
        return self.request("/api/v1/verifications", {"projectId": self.project_id, "type": "coordinate_completion", "profileId": profile_id, "observations": observations}, idempotency_key)

    def verify_evidence(self, profile_id, artifacts, idempotency_key):
        return self.request("/api/v1/verifications", {"projectId": self.project_id, "type": "execution_evidence", "profileId": profile_id, "artifacts": artifacts}, idempotency_key)

    def get(self, verification_id):
        return self.request("/api/v1/verifications/" + urllib.parse.quote(verification_id, safe=""))

    def export(self, verification_id):
        return self.request("/api/v1/verifications/" + urllib.parse.quote(verification_id, safe="") + "/export")

    def lookup(self, reference):
        return self.request("/api/v1/requests?" + urllib.parse.urlencode({"projectId": self.project_id, "reference": reference}))

    def list(self, page=0, conclusion="", search=""):
        return self.request("/api/v1/verifications?" + urllib.parse.urlencode({"projectId": self.project_id, "page": page, "conclusion": conclusion, "search": search}))


if __name__ == "__main__":
    try:
        with open(sys.argv[1], encoding="utf-8") as source:
            materials = json.load(source)
        client = ChoiceProofCloud(os.environ["CHOICEPROOF_API_KEY"], os.environ.get("CHOICEPROOF_PROJECT_ID", materials.get("projectId", "")), os.environ.get("CHOICEPROOF_URL", "https://choiceproof.dev"))
        print(json.dumps(client.verify(os.environ.get("CHOICEPROOF_PROFILE_ID", materials.get("profileId", "")), materials["observations"], os.environ["CHOICEPROOF_IDEMPOTENCY_KEY"])))
    except CloudAPIError as error:
        print(json.dumps({"code": error.code, "requestId": error.request_id, "retryAt": error.retry_at, "message": str(error)}), file=sys.stderr)
        sys.exit(1)
    except (OSError, ValueError, KeyError, IndexError) as error:
        print("Client setup failed: " + str(error), file=sys.stderr)
        sys.exit(2)
