# Firmador del chat web de Vocero **con identidad avalada**, para Python.
#
# Sirve para una aplicacion con sesion —FastAPI, Django, Flask— donde el
# servidor **ya sabe quien esta del otro lado**. Es el equivalente de
# `node-con-identidad.js`, y firma exactamente lo mismo: si los dos lados no
# arman la cadena igual, la firma no valida y el sintoma es un `AVAL_INVALIDO`
# que no dice por que.
#
# La diferencia con el firmador anonimo no es comodidad. Sobre un nombre
# **declarado** —que lo manda el navegador y cualquiera lo cambia por el de
# otro— un agente no deberia actuar; sobre uno **avalado** si. Es lo que separa
# "dice llamarse Ana" de "es Ana", y en atencion decide si se le puede dar
# informacion de la cuenta.

# Las anotaciones quedan diferidas: asi el archivo se puede copiar tal cual a un
# proyecto viejo sin que `str | None` reviente en el import.
from __future__ import annotations

import hashlib
import hmac
import os
import time
import uuid

SITE_KEY = os.environ["VOCERO_SITE_KEY"]
SECRET = os.environ["VOCERO_SECRET"]

# Que falte una variable no puede pasar en silencio: sin esto se firma sobre la
# cadena vacia y sale una firma perfectamente formada y perfectamente inutil.
# Del otro lado solo se ve `AVAL_INVALIDO`, que no dice cual de las dos falta.
if not SITE_KEY or not SECRET:
    raise RuntimeError("Faltan VOCERO_SITE_KEY o VOCERO_SECRET")
if SITE_KEY.startswith("wcs_"):
    raise RuntimeError("VOCERO_SITE_KEY tiene la clave secreta: la publica empieza con wc_")


def _firmar(*partes: str) -> str:
    mensaje = "\n".join(partes).encode()
    return hmac.new(SECRET.encode(), mensaje, hashlib.sha256).hexdigest()


def avalar(usuario=None, visitor_id: str | None = None) -> dict:
    """Lo que hay que devolver ante un POST a la direccion que declara la
    etiqueta en `data-vocero-signer`.

    `usuario` sale de **la sesion del servidor**, nunca del cuerpo del pedido:
    tomandolo de lo que manda el navegador, cualquiera se avala como cualquiera
    y la firma deja de probar nada.

    Se espera con los atributos `id`, `name` y `email`; con un diccionario
    tambien anda si se lo adapta.
    """
    visitor_id = visitor_id or f"v_{uuid.uuid4()}"
    issued_at = int(time.time())

    respuesta = {
        "visitorId": visitor_id,
        "issuedAt": issued_at,
        "signature": _firmar(SITE_KEY, visitor_id, str(issued_at)),
    }

    # Sin sesion se contesta solo el aval: el chat abre igual, con quien escribe
    # como visitante anonimo. Es lo que corresponde en las pantallas publicas de
    # la aplicacion, como el ingreso.
    if usuario is None:
        return respuesta

    externo = str(getattr(usuario, "id", "") or "")
    correo = str(getattr(usuario, "email", "") or "")

    # El nombre viaja pero **no se firma**: lo avalado son los identificadores.
    # Firmarlo obligaria a re-firmar cada vez que alguien se corrige el
    # apellido, y no aporta: lo que decide si se le puede dar informacion de una
    # cuenta es el identificador, no como se llama.
    respuesta["identity"] = {
        "externalId": externo,
        "name": getattr(usuario, "name", None),
        "email": correo,
        "issuedAt": issued_at,
        "signature": _firmar(SITE_KEY, visitor_id, externo, correo, str(issued_at)),
    }

    return respuesta


# Ejemplo con FastAPI. Lo que importa es de donde sale `usuario`.
#
#     @router.post("/vocero-aval")
#     async def vocero_aval(
#         cuerpo: dict | None = None,
#         usuario=Depends(usuario_actual_opcional),
#     ):
#         return avalar(usuario, (cuerpo or {}).get("visitorId"))
#
# **Opcional y no obligatorio**: en las pantallas publicas no hay sesion y el
# chat tiene que abrir igual, como anonimo. Con la dependencia obligatoria, el
# chat del ingreso deja de funcionar.
#
# Y ponerle un **limite de frecuencia**: es una direccion publica y, con la
# firma obligatoria, la unica superficie que le queda a un atacante.
