"""Server-side Python 3.10+; pip install requests. No network call on import."""
import os
import time
import uuid
from urllib.parse import urlsplit, urlencode, parse_qsl
import requests

class AgilinkError(RuntimeError):
    def __init__(self, status, value, request_id, intent_id):
        self.status = status
        self.code = (value or {}).get('code')
        self.resource = (value or {}).get('resource')
        self.request_id = request_id
        self.intent_id = intent_id
        super().__init__(f'Agilink HTTP {status}; {self.code}; requestId={request_id}')

class AgilinkClient:
    def __init__(self, bearer=None, environment="staging"):
        targets = {"staging": "https://api.staging.agilink.io", "production": "https://api.agilink.io"}
        if environment not in targets:
            raise ValueError("Ambiente inválido")
        self.origin = targets[environment]
        self.bearer = bearer or os.environ.get('AGILINK_API_KEY')
        if not self.bearer:
            raise ValueError('Defina a credencial no ambiente seguro do servidor.')

    def request(self, path, method='GET', body=None, revision=None, intent_id=None, binary=False):
        if not path.startswith('/') or path.startswith('//') or any(c in path for c in ('\\', '\r', '\n', '#')):
            raise ValueError('Caminho inválido')
        intent_id = intent_id or str(uuid.uuid4())
        headers = {'Authorization': 'Bearer ' + self.bearer}
        if method != 'GET':
            headers['Idempotency-Key'] = intent_id
        if revision:
            headers['If-Match'] = '"' + revision + '"'
        for attempt in range(3):
            try:
                response = requests.request(method, self.origin + '/v1' + path, headers=headers,
                                            json=body, timeout=30, allow_redirects=False)
            except requests.RequestException:
                if attempt < 2:
                    time.sleep(2 ** attempt)
                    continue
                raise AgilinkError(0, {'code': 'outcome_unknown'}, None, intent_id) from None
            if 200 <= response.status_code < 300:
                return {'data': None if response.status_code == 204 else response.content if binary else response.json(),
                        'etag': response.headers.get('ETag'), 'location': response.headers.get('Location'),
                        'status': response.status_code, 'intent_id': intent_id}
            try:
                value = response.json().get('error', {})
            except ValueError:
                value = {'code': 'invalid_response'}
            error = AgilinkError(response.status_code, value, response.headers.get('X-Request-Id'), intent_id)
            if (response.status_code == 429 or response.status_code >= 500 or (response.status_code == 409 and value.get('code') == 'operation_in_progress' and response.headers.get('Retry-After'))) and attempt < 2:
                try:
                    delay = float(response.headers.get('Retry-After', 2 ** attempt))
                except ValueError:
                    delay = 2 ** attempt
                time.sleep(min(60, max(1, delay)))
                continue
            raise error

    def pages(self, path):
        cursor = None
        while True:
            url = urlsplit(path)
            query = dict(parse_qsl(url.query))
            query['limit'] = '100'
            if cursor:
                query['cursor'] = cursor
            response = self.request(url.path + '?' + urlencode(query))['data']
            yield response['data']
            cursor = response['pagination']['nextCursor']
            if not cursor:
                break

    def wait_operation(self, operation_id, timeout_seconds=90):
        deadline = time.monotonic() + timeout_seconds
        while time.monotonic() < deadline:
            result = self.request('/operations/' + operation_id)['data']['data']
            if result['status'] in ('succeeded', 'partially_succeeded', 'failed', 'cancelled'):
                return result
            time.sleep(1.5)
        raise TimeoutError('Consulte a mesma operação antes de reenviar.')
