#!/usr/bin/env python3
"""
benchmarks.py - Benchmarks oficiales de SiPi (item #65 del feedback).

Este archivo estaba REFERENCIADO por 'sipi_cli.py' (el subcomando
'sipi benchmarks' hace `subprocess.run([sys.executable,
os.path.join(AQUI, "benchmarks.py")] + args)`) y documentado en
'docs/CHANGELOG.md' (version 41.27) desde hace varias versiones, pero
el archivo nunca habia llegado a este paquete -- 'sipi benchmarks'
fallaba siempre con "No se encontro 'benchmarks.py' en esta carpeta.".

Mide el motor REAL en las 6 categorias que pide el feedback original:
bucles, funciones/recursion, strings, listas, archivos y concurrencia
(hilos reales) -- cada benchmark es un programa .sipi real, generado
aca mismo y corrido contra el interprete real ('sipi.py', el mismo
que usa cualquier programa .sipi), nunca numeros inventados.

Uso:
    python3 benchmarks.py                       Corre las 6 categorias, 1 repeticion
    python3 benchmarks.py --repeticiones 5       Promedia 5 corridas por categoria
    python3 benchmarks.py --json salida.json     Ademas guarda el resultado en JSON
    python3 benchmarks.py --sin-cache            No usa el cache .sipic al medir (por defecto ya se usa --sin-cache, ver mas abajo)

Nota de diseño: cada benchmark se corre con '--sin-cache' contra
'sipi.py' para medir parseo+ejecucion real (no solo la ejecucion con
el .sipic ya generado de una corrida anterior, que daria un numero
optimista y no comparable entre maquinas/versiones).
"""
import argparse
import contextlib
import io
import json
import os
import statistics
import subprocess
import sys
import tempfile
import time

AQUI = os.path.dirname(os.path.abspath(__file__))


def _ruta_motor():
    """Prefiere 'sipi.py' (desarrollo); si esta carpeta es una
    distribucion ya protegida, usa 'sipi_protegido.py' -- mismo
    criterio que ya usa el resto del proyecto (editor, CLI)."""
    normal = os.path.join(AQUI, "sipi.py")
    if os.path.exists(normal):
        return normal
    protegido = os.path.join(AQUI, "sipi_protegido.py")
    if os.path.exists(protegido):
        return protegido
    return None


PROGRAMA_BUCLES = """programa "Benchmark bucles"
variable total = 0
variable i = 0
mientras i < 200000
    sumar total i
    sumar i 1
fin
decir "{total}"
"""

PROGRAMA_FUNCIONES = """programa "Benchmark funciones y recursion"

funcion fibonacci(n)
    si n < 2
        devolver n
    fin
    llamar_valor fibonacci(n - 1) -> a
    llamar_valor fibonacci(n - 2) -> b
    devolver a + b
fin

llamar_valor fibonacci(22) -> resultado
decir "{resultado}"
"""

PROGRAMA_STRINGS = """programa "Benchmark strings"
variable texto = ""
variable i = 0
mientras i < 20000
    texto_reemplazar "hola-mundo-de-sipi" "-" "_" -> pedazo
    sumar i 1
fin
decir "listo"
"""

PROGRAMA_LISTAS = """programa "Benchmark listas"
lista_crear numeros
variable i = 0
mientras i < 50000
    lista_agregar numeros i
    sumar i 1
fin
suma_lista numeros -> total
decir "{total}"
"""

PROGRAMA_ARCHIVOS = """programa "Benchmark archivos"
variable i = 0
mientras i < 500
    crear_archivo "bench_tmp_{i}.txt" "contenido de prueba"
    leer_archivo "bench_tmp_{i}.txt" -> contenido
    borrar_archivo "bench_tmp_{i}.txt"
    sumar i 1
fin
decir "listo"
"""

PROGRAMA_HILOS = """programa "Benchmark concurrencia (hilos reales)"

funcion trabajo(n)
    variable total = 0
    variable i = 0
    mientras i < 100000
        sumar total i
        sumar i 1
    fin
    devolver total
fin

hilo_crear trabajo(1) -> h1
hilo_crear trabajo(2) -> h2
hilo_crear trabajo(3) -> h3
hilo_crear trabajo(4) -> h4
hilo_esperar_todos
decir "listo"
"""

CATEGORIAS = [
    ("bucles", PROGRAMA_BUCLES),
    ("funciones_recursion", PROGRAMA_FUNCIONES),
    ("strings", PROGRAMA_STRINGS),
    ("listas", PROGRAMA_LISTAS),
    ("archivos", PROGRAMA_ARCHIVOS),
    ("concurrencia_hilos", PROGRAMA_HILOS),
]


def _correr_una_vez(ruta_motor, ruta_programa, carpeta_trabajo):
    """Corre un programa .sipi contra el motor real con --sin-cache
    (para medir parseo+ejecucion de punta a punta, no solo el
    .sipic de una corrida anterior) y devuelve el tiempo en segundos.
    La salida real del programa se descarta (no nos interesa para
    medir tiempo, y así no ensucia la tabla de resultados), pero
    cualquier error real SI se muestra, para no esconder un
    benchmark roto detrás de un número."""
    inicio = time.perf_counter()
    resultado = subprocess.run(
        [sys.executable, ruta_motor, "--sin-cache", ruta_programa],
        cwd=carpeta_trabajo,
        capture_output=True,
        text=True,
        encoding="utf-8",
        errors="replace",
    )
    duracion = time.perf_counter() - inicio
    if resultado.returncode != 0:
        raise RuntimeError(
            f"el benchmark termino con error (codigo {resultado.returncode}):\n{resultado.stderr.strip()}"
        )
    return duracion


def correr_benchmarks(repeticiones=1, categorias=None):
    ruta_motor = _ruta_motor()
    if not ruta_motor:
        print("[SiPi] No se encontro 'sipi.py' ni 'sipi_protegido.py' junto a benchmarks.py.")
        sys.exit(1)

    seleccion = categorias or [nombre for nombre, _ in CATEGORIAS]
    resultados = {}

    with tempfile.TemporaryDirectory(prefix="sipi_benchmarks_") as carpeta:
        for nombre, contenido in CATEGORIAS:
            if nombre not in seleccion:
                continue
            ruta_programa = os.path.join(carpeta, f"{nombre}.sipi")
            with open(ruta_programa, "w", encoding="utf-8") as f:
                f.write(contenido)

            tiempos = []
            error = None
            for _ in range(max(1, repeticiones)):
                try:
                    tiempos.append(_correr_una_vez(ruta_motor, ruta_programa, carpeta))
                except RuntimeError as e:
                    error = str(e)
                    break

            if error:
                resultados[nombre] = {"error": error}
                print(f"[SiPi] {nombre}: ERROR -- {error}")
                continue

            promedio = statistics.mean(tiempos)
            resultados[nombre] = {
                "repeticiones": len(tiempos),
                "tiempos_segundos": [round(t, 4) for t in tiempos],
                "promedio_segundos": round(promedio, 4),
                "minimo_segundos": round(min(tiempos), 4),
                "maximo_segundos": round(max(tiempos), 4),
            }
            print(f"[SiPi] {nombre:<22} promedio: {promedio:.4f}s  (min {min(tiempos):.4f}s / max {max(tiempos):.4f}s, {len(tiempos)} corrida(s))")

    return resultados


def main():
    parser = argparse.ArgumentParser(
        description="Benchmarks oficiales de SiPi (bucles, funciones, strings, listas, archivos, hilos)."
    )
    parser.add_argument("--repeticiones", type=int, default=1,
                         help="Cuantas veces correr cada categoria, para promediar (default: 1).")
    parser.add_argument("--json", dest="ruta_json", default=None,
                         help="Ademas de imprimir la tabla, guarda el resultado completo en este archivo JSON.")
    parser.add_argument("--categoria", action="append", dest="categorias", default=None,
                         help="Corre solo esta categoria (repetible). Por defecto corre las 6.")
    parser.add_argument("--sin-cache", action="store_true",
                         help="No tiene efecto adicional: los benchmarks ya siempre corren con --sin-cache para ser comparables.")
    args = parser.parse_args()

    print("[SiPi] Corriendo benchmarks oficiales contra el motor real...")
    resultados = correr_benchmarks(repeticiones=args.repeticiones, categorias=args.categorias)

    if args.ruta_json:
        with open(args.ruta_json, "w", encoding="utf-8") as f:
            json.dump(resultados, f, ensure_ascii=False, indent=2)
        print(f"[SiPi] Resultado guardado en: {args.ruta_json}")

    hubo_error = any("error" in r for r in resultados.values())
    sys.exit(1 if hubo_error else 0)


if __name__ == "__main__":
    main()
