"""
Salud de los servicios de IA por tenant — el canal por el que un fallo de fondo
llega a la pantalla.

La generación anticipada y el pre-recorte de fondo los lanza el organizador, sin
nadie delante. El día que la cuenta se quedó sin saldo, el único rastro fueron
unas líneas de log en el servidor: en pantalla no pasaba nada, las fotos se
organizaban con normalidad y simplemente no aparecían las versiones IA. Aquí se
guarda el último fallo definitivo de cada tenant y `/api/organizer/status` lo
devuelve junto al resto del estado — el front ya sondea ese endpoint, así que no
hace falta ni un endpoint ni un sondeo más.

Dos reglas que definen lo que se guarda:

- **Sólo fallos definitivos.** Quien registra lo hace DESPUÉS de `with_retries`,
  nunca dentro. Los 5xx esporádicos del proveedor bajo carga se resuelven solos
  en el siguiente intento; encender un aviso por cada uno enseñaría a ignorarlos,
  que es justo lo que no puede pasar con el que sí importa.
- **Un éxito lo borra.** El estado responde «¿funciona ahora?», no «¿falló alguna
  vez?». Y caduca por su cuenta (`_TTL_SECS`): un fallo de hace horas del que
  nadie ha vuelto a saber no debe recibir a quien abre la aplicación por la
  tarde.

`code` distingue el caso que no se arregla reintentando (`no_credits`) del resto
(`error`), porque son dos acciones distintas: recargar la cuenta o mirar el log.
El texto crudo del proveedor se queda en el log y NO viaja al front: nombra al
servicio externo, y eso no tiene por qué acabar en la pantalla de un cliente.
"""
import logging
import time
from typing import Optional

logger = logging.getLogger(__name__)

# Pasado este tiempo sin noticias, el último fallo deja de anunciarse: si nada
# lo ha vuelto a intentar en media hora, ya no describe el estado actual.
_TTL_SECS = 30 * 60

# Tareas que reportan aquí. Sirven para decir *qué* no salió, no para clasificar.
TASK_IA = "ia"        # generación de versiones IA
TASK_NOBG = "nobg"    # recorte de fondo (canvas y reemplazo de fondo)
TASK_HEADS = "heads"  # protección de caras del portal de cliente

_state: dict[str, dict] = {}


def _classify(exc: BaseException) -> str:
    """`no_credits` es el único caso que merece su propio mensaje: el proveedor
    responde 400 y no hay reintento que lo arregle — hay que recargar la cuenta."""
    msg = str(getattr(exc, "detail", exc)).lower()
    return "no_credits" if "insufficient credits" in msg else "error"


def record_failure(short_code: str, task: str, exc: BaseException) -> None:
    """Registra un fallo YA definitivo (reintentos agotados o no aplicables)."""
    if not short_code:
        return
    code = _classify(exc)
    prev = _state.get(short_code)
    # El contador acumula el episodio, no la vida del proceso: un lote de 30
    # fotos sin saldo es un aviso que dice 30, no treinta avisos.
    count = prev["count"] + 1 if prev and prev["code"] == code else 1
    _state[short_code] = {
        "code": code,
        "task": task,
        "count": count,
        "at": time.time(),
    }


def record_success(short_code: str) -> None:
    """Una llamada que sí funcionó: el servicio está vivo, se borra el aviso."""
    if short_code and _state.pop(short_code, None):
        # `audit`: hito que sí va al audit — cierra el episodio de fallo.
        logger.info("🟢 [%s] servicios IA operativos de nuevo", short_code, extra={"audit": True})


def get_status(short_code: str) -> Optional[dict]:
    """Estado para el front, o `None` si no hay nada que contar.

    Devuelve `at` en segundos de antigüedad en vez de una marca de tiempo: el
    front sólo necesita saber si es el mismo episodio del que ya avisó, y una
    edad no depende de que el reloj del navegador coincida con el del servidor.
    """
    st = _state.get(short_code)
    if not st:
        return None
    age = time.time() - st["at"]
    if age > _TTL_SECS:
        del _state[short_code]
        return None
    return {
        "code": st["code"],
        "task": st["task"],
        "count": st["count"],
        "seconds_ago": int(age),
        # Identifica el episodio sin exponer la hora del servidor: cambia con
        # cada fallo nuevo y es lo que el front compara para no repetir avisos.
        "episode": int(st["at"] * 1000),
    }
