"""Python 3.10+, biblioteca padrão; executar no servidor consumidor.

CRM_BASE_URL=https://crm.sendero.network/api/integracoes/v1
CRM_API_KEY deve ser configurada no ambiente seguro, sem gravar a chave aqui.
CRM_ALL_PAGES=1 percorre todas as páginas; omitido consulta apenas a primeira.
Não colocar a chave em URL, código público ou logs. Usar HTTPS em produção.
"""
import json
import os
import re
import time
from decimal import Decimal
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener


class ApiError(Exception):
    def __init__(self, status, code, request_id=None, retry_after=None):
        self.status = status if isinstance(status, int) else 0
        self.code = code if isinstance(code, str) and re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]{0,47}", code) else "CONSULTA_FALHOU"
        self.request_id = request_id if isinstance(request_id, str) and re.fullmatch(r"[a-fA-F0-9-]{36}", request_id) else None
        self.retry_after = int(retry_after) if isinstance(retry_after, str) and re.fullmatch(r"\d{1,6}", retry_after) else None
        super().__init__(f"Consulta interrompida (HTTP {self.status}, {self.code}).")


class SemRedirecionamento(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        # Impede encaminhar Authorization a outro endereço por redirecionamento.
        return None


def criar_cliente(base_url=None, key=None, timeout=15, opener=None):
    base_url = base_url or os.environ.get("CRM_BASE_URL", "https://crm.sendero.network/api/integracoes/v1")
    token = (os.environ.get("CRM_API_KEY", "") if key is None else key).strip()
    if not re.fullmatch(r"[a-fA-F0-9]{64}", token):
        raise ValueError("Configure CRM_API_KEY no ambiente seguro do consumidor.")
    base = urlsplit(base_url)
    if (base.scheme not in ("http", "https") or not base.netloc or base.username or base.password
            or base.query or base.fragment or base.path.rstrip("/") != "/api/integracoes/v1"):
        raise ValueError("CRM_BASE_URL deve ser a URL completa da API, terminando em /api/integracoes/v1.")
    client = opener or build_opener(SemRedirecionamento())

    def consultar_pagina(resource="/creditos", params=None):
        if not re.fullmatch(r"/[a-zA-Z0-9_/-]+", resource) or "//" in resource or ".." in resource:
            raise ValueError("Use um caminho relativo, por exemplo /creditos ou /dados/prospects.")
        values = {"fields": "id,name", "limit": "50"} if params is None else params
        query = {}
        for name, value in values.items():
            if re.search(r"token|secret|credential|authorization|api.?key", name, re.I):
                raise ValueError("Credenciais não podem ser enviadas na URL.")
            if value is not None:
                query[name] = json.dumps(value, ensure_ascii=False, separators=(",", ":")) if isinstance(value, (list, dict, bool)) else str(value)
        url = base_url.rstrip("/") + resource + ("?" + urlencode(query) if query else "")
        request = Request(url, method="GET", headers={"Authorization": "Bearer " + token, "Accept": "application/json"})
        try:
            # timeout limita a espera de operações de socket; não é um prazo para toda a exportação.
            with client.open(request, timeout=timeout) as response:
                return json.loads(response.read().decode("utf-8"), parse_float=Decimal)
        except HTTPError as error:
            try:
                payload = json.loads(error.read().decode("utf-8"))
                code = payload.get("error", {}).get("code")
                request_id = payload.get("requestId")
            except (ValueError, AttributeError, UnicodeError):
                code, request_id = "RESPOSTA_INVALIDA", None
            finally:
                error.close()
            raise ApiError(error.code, code, request_id, error.headers.get("Retry-After")) from None
        except (TimeoutError, URLError):
            raise ApiError(0, "FALHA_DE_CONEXAO_OU_TIMEOUT") from None
        except (ValueError, UnicodeError):
            raise ApiError(200, "RESPOSTA_INVALIDA") from None
    return consultar_pagina


def consultar_paginas(consultar_pagina, resource="/creditos", params=None, max_pages=10000, interval=1.1):
    if not isinstance(max_pages, int) or max_pages < 1 or interval < 0:
        raise ValueError("Limites de paginação inválidos.")
    values = {"fields": "id,name", "limit": "50"} if params is None else dict(params)
    cursor, cursors = None, set()
    for number in range(1, max_pages + 1):
        page = consultar_pagina(resource, {**values, **({"cursor": cursor} if cursor else {})})
        pagination = page.get("pagination", {})
        if not isinstance(page.get("items"), list) or not isinstance(pagination.get("hasMore"), bool):
            raise ApiError(200, "PAGINA_INVALIDA")
        yield page
        if not pagination["hasMore"]:
            return
        next_cursor = pagination.get("nextCursor")
        if not isinstance(next_cursor, str) or not next_cursor or next_cursor in cursors:
            raise ApiError(200, "CURSOR_INVALIDO")
        cursors.add(next_cursor)
        cursor = next_cursor
        # Outras aplicações usando a mesma chave também contam para 60 consultas/minuto.
        # HTTP 429 interrompe a execução; respeite Retry-After antes de reiniciar.
        if number < max_pages:
            time.sleep(interval)
    raise ApiError(0, "LIMITE_DE_PAGINAS")


if __name__ == "__main__":
    try:
        consultar = criar_cliente()
        if os.environ.get("CRM_ALL_PAGES") == "1":
            pages, items = 0, 0
            for page in consultar_paginas(consultar):
                # Processe page["items"] no seu sistema; não registre dados pessoais.
                # Dinheiro chega como string: Decimal(valor) preserva a precisão.
                # Não converta CPF/CNPJ ou códigos legados em números.
                pages += 1
                items += len(page["items"])
            print(json.dumps({"paginas": pages, "itens": items, "completo": True}))
        else:
            page = consultar()
            print(json.dumps({"itens": len(page["items"]), "temProximaPagina": page["pagination"]["hasMore"]}))
    except ApiError as error:
        print(json.dumps({"erro": error.code, "http": error.status, "requestId": error.request_id, "tentarAposSegundos": error.retry_after}))
        raise SystemExit(1) from None
    except Exception:
        print(json.dumps({"erro": "CONFIGURACAO_OU_RESPOSTA_INVALIDA", "orientacao": "Confira as variáveis do ambiente seguro e o contrato da consulta."}))
        raise SystemExit(1) from None
