"""Orchestra API v1, Python 3.10+, standard library only. Keep keys out of source control."""
import json
import time
import uuid
from urllib.parse import urlsplit, urlencode, quote
from urllib.request import Request, build_opener, HTTPRedirectHandler
from urllib.error import HTTPError


class OrchestraAPIError(Exception):
    def __init__(self, status, error, retry_after=0):
        super().__init__(error.get('message', f'Orchestra API returned HTTP {status}'))
        self.status = status
        self.code = error.get('code', 'http_error')
        self.request_id = error.get('requestId')
        self.retry_after = retry_after


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


class Orchestra:
    def __init__(self, api_key=None, base_url='https://orchestra.money', retries=2):
        url = urlsplit(base_url)
        if (url.scheme != 'https' and not (url.scheme == 'http' and url.hostname in ('localhost', '127.0.0.1', '::1'))) or not url.hostname or url.username or url.password or url.query or url.fragment or url.path not in ('', '/'):
            raise ValueError('Use an HTTPS origin or localhost HTTP.')
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key
        self.retries = min(3, max(0, retries))
        self.opener = build_opener(_NoRedirect())

    def request(self, path, method='GET', body=None, idempotency_key=None):
        if not path.startswith('/') or path.startswith('//') or any(c in path for c in ('..', '://', '#', '\\', '\n', '\r', ' ')):
            raise ValueError('Use a relative resource path, for example /insights.')
        headers = {'Accept': 'application/json'}
        if self.api_key:
            headers['Authorization'] = f'Bearer {self.api_key}'
        data = None if body is None else json.dumps(body).encode()
        if data is not None:
            headers['Content-Type'] = 'application/json'
        if method != 'GET':
            headers['Idempotency-Key'] = idempotency_key or str(uuid.uuid4())
        for attempt in range(self.retries + 1):
            request = Request(self.base_url + '/api/v1' + path, data=data, headers=headers, method=method)
            try:
                response = self.opener.open(request, timeout=20)
            except HTTPError as error:
                response = error
            with response:
                raw = response.read(8 * 1024 * 1024 + 1)
                if len(raw) > 8 * 1024 * 1024:
                    raise ValueError('API response exceeds 8 MiB.')
                try:
                    payload = json.loads(raw)
                except (ValueError, UnicodeDecodeError):
                    payload = {}
                if 200 <= response.code < 300:
                    return payload.get('data')
                try:
                    retry = max(0, min(60, int(response.headers.get('Retry-After', '1'))))
                except ValueError:
                    retry = 1
                if method == 'GET' and attempt < self.retries and response.code in (429, 502, 503, 504):
                    time.sleep(max(retry, 2 ** attempt))
                    continue
                raise OrchestraAPIError(response.code, payload.get('error', {}), retry)

    def cameras(self, **query):
        return self.request('/cameras?' + urlencode(query))

    def insights(self, camera_id=None):
        return self.request('/insights' + ('?' + urlencode({'cameraId': camera_id}) if camera_id else ''))

    def world(self):
        return self.request('/world/native')

    def count_status(self):
        return self.request('/counts/status')

    def count_cameras(self):
        return self.request('/counts/cameras')

    def count_map(self):
        return self.request('/counts/map')

    def counts(self, camera_id, **query):
        return self.request('/counts/' + quote(camera_id, safe='') + '?' + urlencode(query))

    def incidents(self, **query):
        return self.request('/incidents?' + urlencode(query))

    def job(self, job_id):
        return self.request('/jobs/' + quote(job_id, safe=''))

    def create_scene_job(self, camera_id, idempotency_key=None):
        return self.request('/jobs', 'POST', {'task': 'scene', 'cameraId': camera_id}, idempotency_key)
