#!/usr/bin/env python3
"""Mide en qué se está yendo la cuota de Claude Code, sesión por sesión.

Las transcripciones viven en ~/.claude/projects/<proyecto>/<sesion>.jsonl y cada
respuesta del modelo trae su consumo exacto. Este guion las suma y las valora con la
tarifa pública de la API, que sirve para comparar una práctica contra otra.

    python3 medir-consumo.py            # resumen de todo
    python3 medir-consumo.py --sesiones # además, el detalle por sesión

Ojo: sólo ve las sesiones de ESTA máquina, y sólo las que no haya limpiado todavía
`cleanupPeriodDays` (30 días por omisión).
"""

import collections
import glob
import json
import os
import sys

# La consola de Windows suele venir en cp1252, donde los emojis de los avisos no
# existen: al imprimirlos Python corta con UnicodeEncodeError. Se pide que reemplace
# lo que no pueda escribir en vez de fallar. (Reportado por María, 13-ago-2026.)
def _salida_tolerante():
    """Que stdout y stderr reemplacen lo que no puedan escribir, en vez de cortar."""
    import io
    for nombre in ("stdout", "stderr"):
        canal = getattr(sys, nombre)
        try:
            canal.reconfigure(errors="replace")
            continue
        except (AttributeError, ValueError):
            pass
        # Python 3.6 no tiene reconfigure, y es justo donde más probable es toparse
        # con una consola en cp1252.
        try:
            setattr(sys, nombre, io.TextIOWrapper(
                canal.buffer, encoding=canal.encoding,
                errors="replace", line_buffering=True))
        except Exception:
            pass


_salida_tolerante()

# Tarifa pública de la API en dólares por millón de tokens:
# (entrada, salida, escribir al caché, leer del caché)
TARIFA = {
    "opus": (15.0, 75.0, 18.75, 1.50),
    "sonnet": (3.0, 15.0, 3.75, 0.30),
    "haiku": (0.80, 4.0, 1.00, 0.08),
}


def tarifa_de(modelo):
    m = (modelo or "").lower()
    for familia, precios in TARIFA.items():
        if familia in m:
            return familia, precios
    return "sonnet", TARIFA["sonnet"]


def costo(uso, precios):
    entrada, salida, escribir, leer = precios
    return (
        uso.get("input_tokens", 0) * entrada
        + uso.get("output_tokens", 0) * salida
        + uso.get("cache_creation_input_tokens", 0) * escribir
        + uso.get("cache_read_input_tokens", 0) * leer
    ) / 1e6


def main():
    detalle = "--sesiones" in sys.argv
    archivos = glob.glob(os.path.expanduser("~/.claude/projects/*/*.jsonl"))
    if not archivos:
        print("No hay transcripciones en ~/.claude/projects/")
        return

    componente = collections.Counter()
    por_modelo = collections.defaultdict(collections.Counter)
    por_tamano = collections.defaultdict(collections.Counter)
    sesiones = []
    total = 0.0
    simulado = 0.0  # el mismo trabajo, pero limpiando antes de los 250k
    TOPE = 250000

    for ruta in archivos:
        peticiones = 0
        ctx_total = 0
        costo_sesion = 0.0
        fechas = []
        modelos = collections.Counter()
        with open(ruta, errors="ignore") as fh:
            for linea in fh:
                try:
                    d = json.loads(linea)
                except ValueError:
                    continue
                if d.get("timestamp"):
                    fechas.append(d["timestamp"])
                if d.get("type") != "assistant":
                    continue
                mensaje = d.get("message") or {}
                uso = mensaje.get("usage")
                if not uso:
                    continue

                familia, precios = tarifa_de(mensaje.get("model"))
                c = costo(uso, precios)
                peticiones += 1
                costo_sesion += c
                modelos[familia] += 1

                entrada, salida, escribir, leer = precios
                componente["releer el contexto"] += (
                    uso.get("cache_read_input_tokens", 0) * leer / 1e6
                )
                componente["escribirlo al cache"] += (
                    uso.get("cache_creation_input_tokens", 0) * escribir / 1e6
                )
                componente["lo que Claude escribe"] += (
                    uso.get("output_tokens", 0) * salida / 1e6
                )
                componente["lo que vos escribis"] += (
                    uso.get("input_tokens", 0) * entrada / 1e6
                )

                por_modelo[familia]["peticiones"] += 1
                por_modelo[familia]["costo"] += c

                ctx = (
                    uso.get("cache_read_input_tokens", 0)
                    + uso.get("cache_creation_input_tokens", 0)
                    + uso.get("input_tokens", 0)
                )
                ctx_total += ctx
                cubeta = min(int(ctx / 100000) * 100, 900)
                por_tamano[cubeta]["peticiones"] += 1
                por_tamano[cubeta]["costo"] += c

                total += c
                uso_topado = dict(uso)
                uso_topado["cache_read_input_tokens"] = min(
                    uso.get("cache_read_input_tokens", 0), TOPE
                )
                simulado += costo(uso_topado, precios)

        if peticiones:
            sesiones.append(
                (
                    costo_sesion,
                    peticiones,
                    ctx_total // peticiones,
                    os.path.basename(os.path.dirname(ruta)),
                    min(fechas)[:10] if fechas else "?",
                    max(fechas)[:10] if fechas else "?",
                    modelos.most_common(1)[0][0],
                )
            )

    print("=== DE QUE ESTA HECHO EL GASTO ===")
    for nombre, monto in componente.most_common():
        print("  {:24} ${:10,.0f}  {:5.1f}%".format(nombre, monto, monto / total * 100))
    print("  {:24} ${:10,.0f}".format("TOTAL", total))

    print("\n=== POR MODELO ===")
    for familia, datos in sorted(por_modelo.items(), key=lambda x: -x[1]["costo"]):
        print(
            "  {:8} {:7,} peticiones  ${:9,.0f}   ${:.2f} por peticion".format(
                familia, int(datos["peticiones"]), datos["costo"],
                datos["costo"] / datos["peticiones"],
            )
        )

    print("\n=== SEGUN CUANTO CONTEXTO ARRASTRABA LA VENTANA ===")
    for cubeta in sorted(por_tamano):
        datos = por_tamano[cubeta]
        print(
            "  {:4d}-{:4d}k  {:7,} pet.  ${:9,.0f}  ${:5.2f}/pet.  {:5.1f}% del gasto".format(
                cubeta, cubeta + 100, int(datos["peticiones"]), datos["costo"],
                datos["costo"] / datos["peticiones"], datos["costo"] / total * 100,
            )
        )

    print("\n=== CUANTO SE AHORRARIA LIMPIANDO ANTES DE LOS {}k ===".format(TOPE // 1000))
    print("  gasto real      ${:11,.0f}".format(total))
    print("  con /clear      ${:11,.0f}".format(simulado))
    print(
        "  AHORRO          ${:11,.0f}   ({:.0f}%)".format(
            total - simulado, (total - simulado) / total * 100
        )
    )

    if detalle:
        sesiones.sort(reverse=True)
        print("\n=== SESIONES (de más cara a menos) ===")
        print(
            "{:>10} {:>11} {:>11} {:>8}  {:10} {:10} {}".format(
                "costo$", "peticiones", "ctx medio", "$/pet", "desde", "hasta", "proyecto"
            )
        )
        for s in sesiones:
            print(
                "{:10,.2f} {:11,} {:10,}k {:8.2f}  {:10} {:10} {} [{}]".format(
                    s[0], s[1], s[2] // 1000, s[0] / s[1], s[4], s[5], s[3][:40], s[6]
                )
            )
    else:
        print("\n(corré con --sesiones para ver el detalle sesión por sesión)")


if __name__ == "__main__":
    main()
