"""Avalio API - cliente oficial para Python (3.8+). Solo stdlib (urllib): sin dependencias.

Uso:
    from avalio import Avalio
    avalio = Avalio(api_key="avk_live_...")        # o avk_test_ para sandbox
    r = avalio.riesgo(cuit="30711039356", monto=4800000, plazo=60)
    print(r["decision"], r["score"], r["reasons"])

Cada metodo devuelve el cuerpo JSON ya parseado (dict). Ante un error de la API lanza AvalioError
con .code (estable), .request_id y .status, para rutear por el code.

Es una API server-to-server: NO uses tu clave avk_live_ en un cliente publico.
"""
import json
import urllib.parse
import urllib.request
import urllib.error

DEFAULT_BASE_URL = "https://avalio.com.ar"


class AvalioError(Exception):
    def __init__(self, message, code=None, request_id=None, status=None):
        super().__init__(message)
        self.code = code            # invalid_api_key, insufficient_credits, rate_limited, ...
        self.request_id = request_id
        self.status = status


class Avalio:
    def __init__(self, api_key, base_url=DEFAULT_BASE_URL, timeout=30):
        if not api_key:
            raise ValueError("Avalio: falta api_key (avk_live_... o avk_test_...).")
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    def _request(self, method, path, body=None, query=None, idempotency_key=None):
        url = self.base_url + path
        if query:
            clean = {k: v for k, v in query.items() if v is not None}
            if clean:
                url += "?" + urllib.parse.urlencode(clean)
        headers = {"Authorization": "Bearer " + self.api_key}
        data = None
        if body is not None:
            data = json.dumps({k: v for k, v in body.items() if v is not None}).encode("utf-8")
            headers["Content-Type"] = "application/json"
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key

        req = urllib.request.Request(url, data=data, headers=headers, method=method)
        try:
            with urllib.request.urlopen(req, timeout=self.timeout) as resp:
                return self._parse(resp.read(), resp.status, resp.headers.get("X-Request-Id"))
        except urllib.error.HTTPError as e:
            # Un 4xx/5xx trae cuerpo JSON con { error, code, request_id }: se parsea igual y se lanza.
            return self._parse(e.read(), e.code, e.headers.get("X-Request-Id"))

    @staticmethod
    def _parse(raw, status, request_id):
        try:
            payload = json.loads(raw.decode("utf-8")) if raw else {}
        except (ValueError, UnicodeDecodeError):
            payload = {"raw": raw.decode("utf-8", "replace") if raw else ""}
        if status >= 400 or payload.get("ok") is False:
            raise AvalioError(
                payload.get("error", "Avalio: HTTP %s" % status),
                code=payload.get("code"),
                request_id=payload.get("request_id") or request_id,
                status=status,
            )
        return payload

    # Riesgo de cliente por CUIT (decision explicable). Cobra 1 credito con clave live (idempotente
    # si pasas idempotency_key). Los CUIT de sandbox responden gratis con cualquier clave.
    def riesgo(self, cuit, monto=None, plazo=None, idempotency_key=None):
        return self._request("POST", "/api/v1/riesgo", {"cuit": cuit, "monto": monto, "plazo": plazo}, idempotency_key=idempotency_key)

    # Evaluar un cheque (efimero, no persiste). Requiere plan Business.
    def evaluate(self, cuit, monto=None, plazo=None, idempotency_key=None):
        return self._request("POST", "/api/v1/evaluate", {"cuit": cuit, "monto": monto, "plazo": plazo}, idempotency_key=idempotency_key)

    # Analizar un cheque, cobrar y PERSISTIR (devuelve un id recuperable). Requiere plan Business.
    def analisis(self, cuit, monto=None, plazo=None, fecha_pago=None, idempotency_key=None):
        return self._request("POST", "/api/v1/analisis", {"cuit": cuit, "monto": monto, "plazo": plazo, "fecha_pago": fecha_pago}, idempotency_key=idempotency_key)

    def get_analisis(self, analisis_id):
        return self._request("GET", "/api/v1/analisis/" + urllib.parse.quote(str(analisis_id)))

    # Resolver el CUIT de una empresa por su razon social (no cobra).
    def buscar_empresa(self, nombre):
        return self._request("GET", "/api/v1/empresas/buscar", query={"nombre": nombre})

    # Riesgo por LOTE (hasta 500 CUITs). Devuelve {job_id, estado_url}.
    def lote(self, cuits=None, csv=None, monto=None, plazo=None):
        return self._request("POST", "/api/v1/riesgo/lote", {"cuits": cuits, "csv": csv, "monto": monto, "plazo": plazo})

    def lote_estado(self, job_id, formato=None):
        return self._request("GET", "/api/v1/riesgo/lote/" + urllib.parse.quote(str(job_id)), query={"formato": formato})

    # Historial de evaluaciones de la empresa (no cobra). formato: json|csv|tsv|html.
    def evaluations(self, limite=None, desde=None, formato=None):
        return self._request("GET", "/api/v1/evaluations", query={"limite": limite, "desde": desde, "formato": formato})

    # Monitoreo de CUITs (Business). Un deterioro dispara el webhook 'emisor.deterioro'.
    def monitor_list(self):
        return self._request("GET", "/api/v1/monitor")

    def monitor_add(self, cuit, cliente=None):
        return self._request("POST", "/api/v1/monitor", {"cuit": cuit, "cliente": cliente})

    def monitor_remove(self, cuit):
        return self._request("DELETE", "/api/v1/monitor", query={"cuit": cuit})

    # Verificar la firma de una respuesta de riesgo ante un tercero. No requiere clave.
    def verificar(self, cuit, score, decision, modo, version, generado, hmac):
        return self._request("POST", "/api/v1/verificar", {
            "cuit": cuit, "score": score, "decision": decision, "modo": modo,
            "version": version, "generado": generado, "hmac": hmac,
        })
