"""Lucky Cobros · servidor mínimo que recibe los avisos (webhooks) y verifica la firma.

Uso (Python 3.8 o superior, solo biblioteca estándar):
    WEBHOOK_SECRET="el-mismo-secreto-que-configuraste" python verificar-firma.py
Escucha en http://localhost:3000/webhooks/cobros (cambiá el puerto con PORT=8080).
(En Windows PowerShell: $env:WEBHOOK_SECRET="..."; python verificar-firma.py)

Reglas que sigue este ejemplo:
  1. La firma se calcula sobre el cuerpo CRUDO (bytes). No lo parsees ni lo vuelvas a serializar antes.
  2. La comparación es en tiempo constante (hmac.compare_digest).
  3. Responde 200 enseguida y procesa después.
  4. Deduplica: el mismo aviso no se procesa dos veces.
"""
import hashlib
import hmac
import json
import os
import re
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SECRETO = os.environ.get("WEBHOOK_SECRET", "")
PUERTO = int(os.environ.get("PORT", "3000"))
RUTA = "/webhooks/cobros"

if not SECRETO:
    sys.exit("Falta WEBHOOK_SECRET: usá el mismo secreto que configuraste con PUT /external/webhook.")

HEX64 = re.compile(r"^[0-9a-f]{64}$")


def firma_valida(secreto: str, cuerpo_crudo: bytes, encabezado) -> bool:
    """True si el encabezado X-Webhook-Signature corresponde a este cuerpo y este secreto."""
    if not encabezado:
        return False
    recibida = re.sub(r"^sha256=", "", encabezado.strip(), flags=re.IGNORECASE).lower()
    if not HEX64.match(recibida):
        return False
    esperada = hmac.new(secreto.encode("utf-8"), cuerpo_crudo, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperada, recibida)


def clave_de_duplicado(aviso: dict):
    """Clave para no procesar dos veces el mismo aviso (None = no hay código para deduplicar)."""
    evento = aviso.get("event", "")
    d = aviso.get("data") or {}
    # Un mismo cobro puede recibir transaction.in_review y después confirmed o refunded: la clave lleva el evento.
    if evento.startswith("transaction."):
        return f"{evento}:{d['id']}" if d.get("id") else None
    if evento.startswith("withdrawal."):
        return f"{evento}:{d['coelsa_id']}" if d.get("coelsa_id") else None
    return None


# En producción guardá las claves en tu base, con un índice único. Acá alcanza con memoria.
procesados = set()
candado = threading.Lock()


def procesar(aviso: dict) -> None:
    evento, d = aviso["event"], aviso.get("data") or {}
    if evento == "transaction.confirmed":
        # Ya está en tu saldo: acreditá d["monto"] al jugador con CUIT d["contraparte_cuil_cuit"].
        print(f"[cobro] {evento} · acreditar $ {d.get('monto')} de {d.get('contraparte_nombre')} "
              f"({d.get('contraparte_cuil_cuit')}) · código {d.get('codigo_coelsa')}", flush=True)
    elif evento in ("transaction.in_review", "transaction.refunded"):
        # No acredites. in_review: queda retenido hasta que tu equipo lo resuelva en el panel (después llega
        # confirmed o refunded con el mismo data.id). refunded: se le devolvió a quien transfirió.
        print(f"[cobro] {evento} · no se acredita · $ {d.get('monto')} de {d.get('contraparte_nombre')} "
              f"· motivo {d.get('motivo') or '-'}", flush=True)
    elif evento.startswith("withdrawal."):
        # Buscá tu retiro abierto por cbu + monto (o por coelsa_id) y actualizá su estado.
        print(f"[retiro] {evento} · status {d.get('status')} · $ {d.get('monto')} a {d.get('cbu')}", flush=True)
    else:
        print(f"[aviso] evento nuevo, lo ignoramos: {evento}", flush=True)


def procesar_sin_repetir(aviso: dict) -> None:
    clave = clave_de_duplicado(aviso)
    if clave:
        with candado:
            if clave in procesados:
                print(f"[aviso] repetido, no se procesa de nuevo: {clave}", flush=True)
                return
            procesados.add(clave)
    procesar(aviso)


class Receptor(BaseHTTPRequestHandler):
    def _responder(self, codigo: int, cuerpo: dict) -> None:
        datos = json.dumps(cuerpo, ensure_ascii=False).encode("utf-8")
        self.send_response(codigo)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(datos)))
        self.end_headers()
        self.wfile.write(datos)

    def _leer_cuerpo(self) -> bytes:
        """Cuerpo crudo, venga con Content-Length o en partes (Transfer-Encoding: chunked)."""
        if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower():
            partes = []
            while True:
                tam = int(self.rfile.readline().split(b";")[0].strip() or b"0", 16)
                if tam == 0:
                    while self.rfile.readline() not in (b"\r\n", b"\n", b""):
                        pass  # encabezados finales, si los hay
                    return b"".join(partes)
                partes.append(self.rfile.read(tam))
                self.rfile.readline()  # fin de línea de cada parte
        return self.rfile.read(int(self.headers.get("Content-Length") or 0))

    def do_GET(self):  # los avisos siempre llegan por POST
        self._responder(404, {"ok": False})

    def do_POST(self):
        if self.path.split("?")[0] != RUTA:
            self._responder(404, {"ok": False})
            return

        cuerpo_crudo = self._leer_cuerpo()

        if not firma_valida(SECRETO, cuerpo_crudo, self.headers.get("X-Webhook-Signature")):
            self._responder(401, {"ok": False, "motivo": "firma inválida"})
            return

        try:
            aviso = json.loads(cuerpo_crudo.decode("utf-8"))
        except ValueError:
            self._responder(400, {"ok": False, "motivo": "JSON inválido"})
            return

        # 1) Respondé 2xx rápido.  2) Procesá después, sin repetir.
        self._responder(200, {"ok": True})
        threading.Thread(target=procesar_sin_repetir, args=(aviso,), daemon=True).start()

    def log_message(self, formato, *args):  # silencia el log por pedido
        pass


if __name__ == "__main__":
    print(f"Esperando avisos en http://localhost:{PUERTO}{RUTA}", flush=True)
    ThreadingHTTPServer(("0.0.0.0", PUERTO), Receptor).serve_forever()
