#!/usr/bin/env python3
"""Avisa cuando una ventana de Claude Code acumuló demasiado contexto.

Se engancha como hook de UserPromptSubmit: cada vez que se manda un mensaje, mira
cuánto contexto arrastra la sesión y —si pasó el umbral— muestra un aviso pidiendo
`/clear`. El aviso va al usuario (`systemMessage`), no al modelo: no gasta tokens.

Por qué existe: medido sobre 49 sesiones reales (1-jul a 13-ago-2026), el 76% del
gasto es releer el contexto acumulado. Una petición a 900k cuesta ocho veces lo que
la misma a 100k. Ver REGLAS-CLAUDE-COSTO.md.

Entra por stdin el JSON del hook (trae `session_id` y `transcript_path`) y sale por
stdout el JSON de respuesta. Ante cualquier problema sale en silencio: un hook que
falla nunca debe estorbar el trabajo.
"""

import json
import os
import sys
import tempfile

# Umbrales en tokens de contexto. El primero es la regla de la casa; el segundo es
# el que ya duele. Se cambian con `--umbral N` (N en tokens) o, para probar a mano,
# con la variable MB_CLAUDE_UMBRAL. El argumento va primero porque un prefijo tipo
# `VAR=x comando` sólo funciona en bash: en Windows el hook corre en PowerShell.
def _umbral():
    if "--umbral" in sys.argv:
        try:
            return int(sys.argv[sys.argv.index("--umbral") + 1])
        except (IndexError, ValueError):
            pass
    try:
        return int(os.environ.get("MB_CLAUDE_UMBRAL", 250000))
    except ValueError:
        return 250000


UMBRAL_AVISO = _umbral()
UMBRAL_GRAVE = UMBRAL_AVISO * 2


def contexto_actual(transcript_path):
    """Tokens de contexto de la última respuesta del modelo, o None si no se sabe.

    Se recorre el archivo de atrás para adelante: la última línea con `usage` es la
    respuesta más reciente, y ahí está lo que se pagó por releer todo.
    """
    try:
        with open(transcript_path, "rb") as f:
            f.seek(0, os.SEEK_END)
            fin = f.tell()
            # Con leer la cola alcanza: son las últimas respuestas, no el historial.
            inicio = max(0, fin - 2000000)
            f.seek(inicio)
            lineas = f.read().split(b"\n")
    except OSError:
        return None

    for linea in reversed(lineas):
        if b'"usage"' not in linea:
            continue
        try:
            d = json.loads(linea)
        except ValueError:
            continue
        # Las respuestas de los subagentes viven en el mismo archivo y traen su
        # propio contexto, mucho más chico. Si se cuelan, el aviso nunca aparece.
        if d.get("isSidechain"):
            continue
        uso = (d.get("message") or {}).get("usage")
        if not uso:
            continue
        return (
            uso.get("cache_read_input_tokens", 0)
            + uso.get("cache_creation_input_tokens", 0)
            + uso.get("input_tokens", 0)
        )
    return None


def _marca(session_id, nivel):
    return os.path.join(
        tempfile.gettempdir(), "mb-claude-costo-{}-{}".format(session_id, nivel)
    )


def ya_se_aviso(session_id, nivel):
    """Evita repetir el mismo aviso en cada mensaje. Devuelve True si ya se dio.

    El estado vive en un archivo por sesión en el directorio temporal; se pierde al
    reiniciar la máquina, que es exactamente lo que se quiere.
    """
    if not session_id:
        return False
    marca = _marca(session_id, nivel)
    if os.path.exists(marca):
        return True
    try:
        open(marca, "w").close()
    except OSError:
        pass
    return False


def olvidar_avisos(session_id):
    """Borra las marcas de una sesión que volvió a estar por debajo del umbral.

    Sin esto, una ventana que hizo `/clear` y volvió a llenarse no recibiría el
    aviso nunca más: el `session_id` no cambia al limpiar, así que la marca de
    "ya avisé" sobreviviría al `/clear` y el hook se quedaría mudo justo cuando
    vuelve a hacer falta.
    """
    if not session_id:
        return
    for nivel in ("aviso", "grave"):
        try:
            os.remove(_marca(session_id, nivel))
        except OSError:
            pass


def main():
    try:
        entrada = json.load(sys.stdin)
    except ValueError:
        return

    transcript = entrada.get("transcript_path")
    if not transcript:
        return

    ctx = contexto_actual(transcript)
    if ctx is None:
        return
    if ctx < UMBRAL_AVISO:
        # La ventana está limpia (recién arrancada o después de un /clear): se
        # olvidan los avisos ya dados para poder volver a avisar si se vuelve a llenar.
        olvidar_avisos(entrada.get("session_id"))
        return

    grave = ctx >= UMBRAL_GRAVE
    nivel = "grave" if grave else "aviso"
    if ya_se_aviso(entrada.get("session_id"), nivel):
        return

    # Este aviso lo lee todo el equipo, programe o no. Va en palabras normales; el
    # número técnico queda al final, entre paréntesis, para quien lo quiera.
    miles = round(ctx / 1000)
    # Costo relativo contra una ventana recién limpiada (~50k de arranque).
    veces = round(ctx / 50000)
    cuanto = (
        "unas {} veces más de lo que costaría".format(veces)
        if veces >= 2
        else "bastante más de lo que costaría"
    )

    if grave:
        texto = (
            "⚠️  Esta conversación ya está muy larga, y Claude la relee entera "
            "cada vez que le escribís.\n"
            "   Cada mensaje que le mandes cuesta {} si la empezaras de nuevo.\n"
            "   Si ya cambiaste de tema: escribí /clear ahora. Si seguís en lo "
            "mismo: terminá lo que estás haciendo, /clear, y arrancá contándole "
            "en tres líneas dónde quedaste.\n"
            "   El porqué, en dos minutos: GUIA-CLAUDE-PARA-TODOS.md   ({}k de "
            "contexto)"
        ).format(cuanto, miles)
    else:
        texto = (
            "\U0001f4a1 Esta conversación ya viene larga. Si estás por empezar "
            "otra cosa, es el momento de escribir /clear.\n"
            "   ({}k de contexto; la regla de la casa son {}k)"
        ).format(miles, UMBRAL_AVISO // 1000)

    json.dump({"systemMessage": texto, "suppressOutput": True}, sys.stdout)


if __name__ == "__main__":
    try:
        main()
    except Exception:
        # Un hook que revienta no debe interrumpir el trabajo de nadie.
        pass
