#!/usr/bin/env python3
"""
sipi_ia_agente.py - SiPi IA Agente: capa de orquestacion sobre sipi_ia.py
pensada para que OTRAS IAs (Claude Code, cualquier agente, cualquier
script) usen SiPi como su herramienta principal para construir software
real y complejo en minutos, no como un generador de un solo archivo.

Por que existe esto y no alcanza con sipi_ia.py:

  sipi_ia.py resuelve una unidad: un pedido -> un programa, con auto-
  correccion. Sirve para un boton "Generar" en el editor. Pero un
  software complejo de verdad casi nunca es un archivo: es varios
  modulos que se llaman entre si, con un plan, con verificacion
  cruzada, y con la posibilidad de rehacer una parte sin tirar todo
  el resto. Este agente agrega esa capa:

  1. PLANIFICACION: dado un objetivo en lenguaje natural, arma un plan
     de que archivos SiPi hacen falta y que hace cada uno, ANTES de
     escribir una linea de codigo (igual que haria un desarrollador
     senior: primero diseño, despues implementacion).

  2. CONSTRUCCION MULTI-ARCHIVO: genera cada archivo del plan,
     ejecutando y auto-corrigiendo cada uno con el motor real de SiPi
     (reutilizando generar_con_auto_correccion de sipi_ia.py), y
     dejando todo organizado en una carpeta de proyecto real.

  3. VERIFICACION CRUZADA: despues de generar todos los archivos,
     corre una pasada de integracion (ejecuta el archivo principal del
     proyecto) y, si algo cruza mal entre modulos, vuelve a corregir
     con el contexto de TODO el proyecto, no solo un archivo aislado.

  4. INTERFAZ DE HERRAMIENTA PARA OTRAS IAs: expone una funcion
     'ejecutar_comando_json' (y un modo CLI --json) que recibe un
     comando estructurado y devuelve un resultado estructurado. Asi
     cualquier agente de IA -- sin necesidad de saber Python ni conocer
     la API interna -- puede invocar SiPi IA como si fuera una
     herramienta mas de su caja (al estilo de una 'tool call'):

         python3 sipi_ia_agente.py --json '{
             "accion": "construir_proyecto",
             "objetivo": "un sistema de reservas de turnos con lista de espera",
             "carpeta": "/ruta/al/proyecto"
         }'

     devuelve un JSON con el plan, cada archivo generado, si se
     verifico sin errores, y cuantos intentos de auto-correccion
     hicieron falta -- todo lo que un agente externo necesita para
     saber si el trabajo esta terminado o si tiene que iterar.

  5. "SIN LIMITACION": en vez de limitar la complejidad del pedido, el
     agente DESCOMPONE lo complejo en partes manejables (multiples
     archivos + multiples rondas de auto-correccion por archivo +
     una ronda final de integracion), que es como se resuelve la
     complejidad en la practica -- no generando un archivo gigante de
     una sola pasada, que es donde a la mayoria de las IAs se les
     complica y empiezan a alucinar sintaxis o perder de vista el
     contexto completo.
"""
import os
import sys
import json
import argparse
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed

import sipi_ia
import sipi_ia_memoria
import sipi_ia_biblioteca


# ------- Planificacion -------

def crear_plan(objetivo, restricciones=None):
    """Le pide al modelo un plan de archivos ANTES de escribir codigo.
    Devuelve una lista de dicts: [{"archivo": "principal.sipi",
    "proposito": "...", "depende_de": [...]}]. El plan siempre incluye
    un archivo 'principal.sipi' que es el punto de entrada real -- el
    que despues se usa para la verificacion de integracion."""
    system = (
        "Sos un arquitecto de software que diseña en el lenguaje SiPi "
        "(programacion en español). Te dan un objetivo y tenes que "
        "planificar en QUE ARCHIVOS conviene dividirlo, sin escribir "
        "codigo todavia. SiPi no tiene un sistema de 'importar' entre "
        "archivos como otros lenguajes -- cada archivo .sipi es "
        "independiente y ejecutable por si solo -- asi que el plan "
        "tiene que reflejar eso: dividir por RESPONSABILIDAD (por "
        "ejemplo 'validaciones.sipi' como referencia de logica que "
        "despues se replica donde haga falta, 'principal.sipi' como "
        "punto de entrada que integra todo lo necesario) en vez de "
        "asumir que un archivo puede llamar funciones de otro.\n\n"
        "Respondé EXCLUSIVAMENTE con JSON valido, sin texto extra, con "
        "esta forma exacta:\n"
        '{"archivos": [{"nombre": "principal.sipi", "proposito": '
        '"que hace este archivo, en una oracion", "notas_diseno": '
        '"decisiones de diseño relevantes para implementarlo bien"}]}\n\n'
        "El primer archivo de la lista SIEMPRE tiene que ser "
        "'principal.sipi': el punto de entrada que un usuario final "
        "ejecutaria. Los demas son opcionales, solo si de verdad ayuda "
        "dividir el diseño (por ejemplo un archivo aparte con datos de "
        "ejemplo, o una version de consola vs. una version con menu). "
        "No generes mas de 5 archivos.\n\n"
        f"Referencia del lenguaje:\n{sipi_ia._referencia_lenguaje_para_prompt()}"
    )
    contenido = f"Objetivo: {objetivo}"
    if restricciones:
        contenido += f"\nRestricciones o preferencias: {restricciones}"
    respuesta = sipi_ia._llamar_modelo([{"role": "user", "content": contenido}], system, max_tokens=1200)
    plan = _parsear_json_de_respuesta(respuesta)
    archivos = plan.get("archivos", [])
    if not archivos:
        archivos = [{"nombre": "principal.sipi", "proposito": objetivo, "notas_diseno": ""}]
    if archivos[0]["nombre"] != "principal.sipi":
        # Se fuerza el contrato "el primero es el punto de entrada" aunque
        # el modelo no lo haya respetado al pie de la letra.
        for a in archivos:
            if a["nombre"] == "principal.sipi":
                archivos.remove(a)
                archivos.insert(0, a)
                break
        else:
            archivos.insert(0, {"nombre": "principal.sipi", "proposito": objetivo, "notas_diseno": ""})
    return archivos


def _parsear_json_de_respuesta(texto):
    """El modelo a veces envuelve el JSON en ```json ... ``` a pesar de
    que se le pide que no lo haga -- esto lo tolera en vez de fallar."""
    texto = texto.strip()
    if texto.startswith("```"):
        texto = texto.split("```")[1]
        if texto.startswith("json"):
            texto = texto[4:]
    try:
        return json.loads(texto)
    except json.JSONDecodeError:
        inicio = texto.find("{")
        fin = texto.rfind("}")
        if inicio != -1 and fin != -1:
            try:
                return json.loads(texto[inicio:fin + 1])
            except json.JSONDecodeError:
                pass
        return {}


# ------- Construccion multi-archivo -------

class ResultadoProyecto:
    def __init__(self, objetivo, plan, archivos_generados, verificacion_integracion, exito):
        self.objetivo = objetivo
        self.plan = plan
        self.archivos_generados = archivos_generados  # lista de dicts con detalle por archivo
        self.verificacion_integracion = verificacion_integracion
        self.exito = exito

    def a_dict(self):
        return {
            "objetivo": self.objetivo,
            "exito": self.exito,
            "plan": self.plan,
            "archivos": self.archivos_generados,
            "verificacion_integracion": self.verificacion_integracion,
        }


def _ruta_motor_sipi():
    aqui = os.path.dirname(os.path.abspath(__file__))
    for nombre in ("sipi.py", "sipi_protegido.py"):
        ruta = os.path.join(aqui, nombre)
        if os.path.exists(ruta):
            return ruta
    raise sipi_ia.ErrorSiPiIA("No se encontro el motor de SiPi (sipi.py) junto al agente.")


def construir_proyecto(objetivo, carpeta_destino, restricciones=None, max_intentos_por_archivo=4,
                        on_evento=None):
    """Funcion principal del agente: planifica y construye un proyecto
    SiPi completo, con auto-correccion por archivo y una verificacion
    final de integracion. on_evento(tipo, datos) es un callback
    opcional para reportar progreso en vivo (util para un panel de UI
    o para loguear el proceso paso a paso)."""

    def _emitir(tipo, **datos):
        if on_evento:
            try:
                on_evento(tipo, datos)
            except Exception:
                pass

    os.makedirs(carpeta_destino, exist_ok=True)
    ruta_motor = _ruta_motor_sipi()

    _emitir("planificando", objetivo=objetivo)
    plan = crear_plan(objetivo, restricciones)
    _emitir("plan_listo", plan=plan)

    # Generacion en PARALELO en vez de un archivo despues de otro: el
    # contexto que cada archivo necesita del resto del proyecto (para
    # mantener nombres/estilo consistentes) es el PLAN completo -- que
    # ya se conoce entero desde el principio, ya que SiPi no tiene
    # 'importar' entre archivos y por lo tanto ningun archivo depende
    # del CODIGO ya generado de otro, solo de saber que existen y para
    # que sirven (su 'proposito'). Antes eso se armaba de forma
    # incremental (esperando a que terminara cada archivo para recien
    # ahi arrancar el siguiente), lo cual serializaba llamadas al
    # modelo que no tenian ninguna dependencia real entre si. Con un
    # pool de hilos, un proyecto de N archivos tarda ~lo que tarda el
    # mas lento de los N (mas la verificacion final), no la suma de
    # los N -- en un proyecto de 4-5 archivos esto es varias veces mas
    # rapido de punta a punta.
    contexto_completo = "\n".join(f"- {a['nombre']}: {a.get('proposito', '')}" for a in plan)
    lock_biblioteca = threading.Lock()  # sipi_ia_biblioteca no es necesariamente thread-safe para escritura

    def _generar_un_archivo(item):
        nombre = item["nombre"]
        proposito = item.get("proposito", "")
        notas = item.get("notas_diseno", "")
        _emitir("generando_archivo", archivo=nombre, proposito=proposito)

        descripcion_completa = f"{proposito}. {notas}".strip()
        otros = contexto_completo.replace(f"- {nombre}: {proposito}\n", "").replace(
            f"- {nombre}: {proposito}", "")
        if otros.strip():
            descripcion_completa += (
                "\n\nContexto de los demas archivos planificados para este mismo "
                "proyecto (para mantener un estilo y vocabulario de variables "
                "consistente, aunque SiPi no permite importar entre archivos):\n"
                + otros
            )

        # Antes de generar desde cero, se busca en la biblioteca de
        # patrones si ya existe una solucion parecida verificada -- si
        # hay una coincidencia fuerte, se usa como punto de partida
        # (codigo_inicial) para que el modelo la ADAPTE en vez de
        # reinventar la rueda, lo cual es mas rapido y mas consistente.
        codigo_base_patron = None
        try:
            with lock_biblioteca:
                similares = sipi_ia_biblioteca.buscar_similares(descripcion_completa, top_n=1, umbral_minimo=0.35)
            if similares:
                codigo_base_patron = similares[0]["codigo"]
                _emitir("patron_reutilizado", archivo=nombre, similitud=similares[0]["similitud"],
                        descripcion_original=similares[0]["descripcion"])
        except Exception:
            codigo_base_patron = None

        def _progreso_intento(numero, codigo, ok, salida, error, nombre=nombre):
            _emitir("intento_archivo", archivo=nombre, intento=numero, ok=ok, error=error)

        resultado = sipi_ia.generar_con_auto_correccion(
            descripcion_completa, ruta_motor, codigo_inicial=codigo_base_patron,
            max_intentos=max_intentos_por_archivo, on_intento=_progreso_intento,
        )

        if resultado.exito:
            try:
                with lock_biblioteca:
                    sipi_ia_biblioteca.guardar_solucion(descripcion_completa, resultado.codigo_final)
            except Exception:
                pass

        ruta_archivo = os.path.join(carpeta_destino, nombre)
        with open(ruta_archivo, "w", encoding="utf-8") as f:
            f.write(resultado.codigo_final)

        calidad = None
        if resultado.exito:
            try:
                calidad = evaluar_calidad(ruta_archivo)
            except Exception:
                calidad = None

        detalle = {
            "archivo": nombre,
            "ruta": ruta_archivo,
            "proposito": proposito,
            "exito": resultado.exito,
            "intentos": len(resultado.intentos),
            "motivo_fin": resultado.motivo_fin,
            "puntaje_calidad": calidad.get("puntaje") if calidad else None,
        }
        _emitir("archivo_terminado", **detalle)
        return detalle

    max_paralelo = min(len(plan), 4) or 1  # tope de 4 llamadas simultaneas al modelo por prudencia/costo
    archivos_generados = []
    with ThreadPoolExecutor(max_workers=max_paralelo) as pool:
        futuros = {pool.submit(_generar_un_archivo, item): item for item in plan}
        for futuro in as_completed(futuros):
            archivos_generados.append(futuro.result())

    # Se reordenan segun el plan original (el paralelismo puede
    # terminarlos en cualquier orden) para que 'principal.sipi' siga
    # apareciendo primero en el resultado, como esperaria quien lee
    # 'archivos_generados'.
    orden = {item["nombre"]: i for i, item in enumerate(plan)}
    archivos_generados.sort(key=lambda a: orden.get(a["archivo"], len(orden)))

    # Verificacion de integracion: se corre el punto de entrada una vez
    # mas, ya con todos los archivos del proyecto en su lugar, por si el
    # diseño esperaba que convivan (por ejemplo archivos de datos que
    # 'principal.sipi' referencia por nombre de archivo).
    principal = next((a for a in archivos_generados if a["archivo"] == "principal.sipi"), None)
    verificacion = {"corrida": False}
    if principal:
        with open(principal["ruta"], encoding="utf-8") as f:
            codigo_principal = f.read()
        ok, salida, error = sipi_ia._ejecutar_codigo_real(ruta_motor, codigo_principal)
        verificacion = {"corrida": True, "ok": bool(ok), "salida": salida, "error": error}
        _emitir("verificacion_integracion", **verificacion)

    exito_general = all(a["exito"] for a in archivos_generados) and verificacion.get("ok", True)

    # Se deja el proyecto ya indexado (sipi_ia_memoria) para que
    # pedidos posteriores sobre este mismo proyecto (agregar un
    # archivo mas, buscar donde esta algo, refactorizar) no tengan que
    # releer y re-analizar todo desde cero.
    try:
        indice = sipi_ia_memoria.IndiceProyecto(carpeta_destino)
        indice.reconstruir(con_resumen_ia=False)
    except OSError:
        pass

    return ResultadoProyecto(objetivo, plan, archivos_generados, verificacion, exito_general)


# ------- Otras capacidades utiles para una IA que programa en SiPi -------

def optimizar_codigo(codigo):
    """Reescribe codigo SiPi que ya funciona para que quede mas prolijo
    y mas facil de mantener (nombres claros, sin repeticion), sin
    cambiarle el comportamiento. Util como paso posterior a la
    generacion inicial, cuando lo que importa ya no es 'que funcione'
    sino 'que quede bien hecho'."""
    system = (
        "Sos un revisor de codigo senior especializado en SiPi. Te dan "
        "un programa que YA FUNCIONA. Tu trabajo es prolijarlo: nombres "
        "de variables mas claros, eliminar repeticion, mejorar la "
        "organizacion -- SIN cambiar que hace el programa ni romper "
        "nada. Devolvé el programa completo en un unico bloque "
        "```sipi ... ```, con un comentario de una linea antes del "
        "bloque explicando que cambiaste.\n\n"
        f"Referencia del lenguaje:\n{sipi_ia._referencia_lenguaje_para_prompt()}"
    )
    respuesta = sipi_ia._llamar_modelo([{"role": "user", "content": f"```sipi\n{codigo}\n```"}], system)
    return respuesta


def generar_pruebas(codigo, descripcion_comportamiento_esperado=""):
    """Genera un segundo programa SiPi que ejercita al primero con
    varios casos y verifica (con 'decir' de resultado esperado vs
    obtenido) que se comporte como se espera -- el equivalente en SiPi
    a un test rapido, ya que el lenguaje no tiene framework de testing
    propio."""
    system = (
        "Sos SiPi IA. Te dan un programa SiPi y tenes que escribir un "
        "SEGUNDO programa SiPi que sirva como prueba: define casos con "
        "distintos valores de entrada, ejecuta la MISMA logica (podes "
        "copiar las funciones relevantes) y usa 'decir' para mostrar "
        "'caso N: esperado X, obtenido Y -> OK' o '-> FALLO' segun "
        "corresponda, comparando con 'si' (SIN 'entonces'). Devolvé "
        "unicamente el programa de prueba en un bloque ```sipi ... ```.\n\n"
        f"Referencia del lenguaje:\n{sipi_ia._referencia_lenguaje_para_prompt()}"
    )
    contenido = f"Programa a probar:\n```sipi\n{codigo}\n```"
    if descripcion_comportamiento_esperado:
        contenido += f"\n\nComportamiento esperado: {descripcion_comportamiento_esperado}"
    respuesta = sipi_ia._llamar_modelo([{"role": "user", "content": contenido}], system, max_tokens=1500)
    return sipi_ia._extraer_bloque_codigo(respuesta)


def diagnosticar_y_explicar_error(codigo, error):
    """Para cuando una IA (o una persona) quiere ENTENDER el error antes
    de simplemente pedir que lo arreglen -- separa diagnostico de
    correccion, porque a veces lo util es solo el diagnostico (por
    ejemplo si el error esta en como se esta usando el programa, no en
    el programa en si)."""
    system = (
        "Sos SiPi IA. Te dan codigo SiPi y un error real que tiro al "
        "ejecutarse. Explicá en 2-4 oraciones, en español simple y "
        "tecnico a la vez: (1) cual es la causa exacta del error, (2) "
        "en que linea o parte del codigo esta, (3) si es un error del "
        "programa o del uso que se le esta dando. NO reescribas el "
        "codigo, solo diagnosticá.\n\n"
        f"Referencia del lenguaje:\n{sipi_ia._referencia_lenguaje_para_prompt()}"
    )
    contenido = f"Codigo:\n```sipi\n{codigo}\n```\n\nError:\n{error}"
    return sipi_ia._llamar_modelo([{"role": "user", "content": contenido}], system, max_tokens=500).strip()


# ------- Evaluacion de calidad (no solo "corre", sino "esta bien escrito") -------
#
# generar_con_auto_correccion (en sipi_ia.py) solo garantiza que el
# programa CORRE sin tirar error -- pero un programa puede correr y
# aun asi estar mal escrito (variables sin usar, nombres poco claros,
# riesgos de seguridad). SiPi ya trae un analizador estatico real
# (sipi.py --revisar, categorias: seguridad/bugs/estilo/sugerencias).
# Esta funcion lo reutiliza para darle a un archivo generado por IA
# una nota de calidad real y objetiva, y opcionalmente le pide al
# modelo que resuelva los hallazgos mas serios (seguridad y bugs)
# antes de darlo por terminado.

def _ruta_motor_sipi_desde_agente():
    return _ruta_motor_sipi()


def evaluar_calidad(ruta_archivo_sipi):
    """Corre el analizador estatico real de SiPi sobre un archivo y
    devuelve un puntaje simple (100 - penalizacion por hallazgo, mas
    peso a seguridad que a estilo) junto con el detalle categorizado.
    No requiere llamar a la API -- es analisis estatico local, gratis
    e instantaneo."""
    import subprocess
    ruta_motor = _ruta_motor_sipi()
    try:
        resultado = subprocess.run(
            [sys.executable, ruta_motor, "--revisar", ruta_archivo_sipi],
            capture_output=True, text=True, timeout=15,
            encoding="utf-8", errors="replace",
            env={**os.environ, "PYTHONIOENCODING": "utf-8"},
        )
    except subprocess.TimeoutExpired:
        return {"puntaje": None, "error": "El analizador tardo demasiado."}

    salida = resultado.stdout
    categorias = {"seguridad": 0, "bugs": 0, "estilo": 0, "sugerencias": 0}
    categoria_actual = None
    detalle = {"seguridad": [], "bugs": [], "estilo": [], "sugerencias": []}
    for linea in salida.splitlines():
        if "SEGURIDAD" in linea:
            categoria_actual = "seguridad"
        elif "POSIBLES BUGS" in linea:
            categoria_actual = "bugs"
        elif "ESTILO" in linea:
            categoria_actual = "estilo"
        elif "SUGERENCIAS" in linea:
            categoria_actual = "sugerencias"
        elif linea.strip().startswith("- ") and categoria_actual:
            categorias[categoria_actual] += 1
            detalle[categoria_actual].append(linea.strip()[2:])

    # Ponderacion: seguridad pesa mucho mas que un detalle de estilo,
    # asi el puntaje refleja gravedad real y no solo cantidad de items.
    penalizacion = (
        categorias["seguridad"] * 20 + categorias["bugs"] * 10
        + categorias["estilo"] * 3 + categorias["sugerencias"] * 2
    )
    puntaje = max(0, 100 - penalizacion)

    return {
        "puntaje": puntaje, "hallazgos_por_categoria": categorias, "detalle": detalle,
        "total_hallazgos": sum(categorias.values()),
    }


def generar_con_calidad_garantizada(descripcion, ruta_motor_sipi, codigo_inicial=None,
                                     max_intentos_ejecucion=4, puntaje_minimo=70, max_rondas_calidad=2,
                                     on_evento=None):
    """Combina generar_con_auto_correccion (que el programa corra) con
    evaluar_calidad (que este bien escrito): si despues de lograr una
    ejecucion limpia el puntaje de calidad queda por debajo de
    puntaje_minimo, le pide al modelo que resuelva especificamente los
    hallazgos de seguridad y bugs (no le importan los de estilo/
    sugerencias para esto, esos son opcionales) y vuelve a verificar,
    hasta max_rondas_calidad veces."""
    def _emitir(tipo, **datos):
        if on_evento:
            try:
                on_evento(tipo, datos)
            except Exception:
                pass

    resultado_ejecucion = sipi_ia.generar_con_auto_correccion(
        descripcion, ruta_motor_sipi, codigo_inicial=codigo_inicial, max_intentos=max_intentos_ejecucion,
    )
    codigo = resultado_ejecucion.codigo_final
    if not resultado_ejecucion.exito:
        return {
            "codigo": codigo, "exito_ejecucion": False, "calidad": None,
            "motivo": resultado_ejecucion.motivo_fin,
        }

    import tempfile
    ronda = 0
    calidad = None
    while ronda < max_rondas_calidad:
        with tempfile.NamedTemporaryFile(mode="w", suffix=".sipi", delete=False, encoding="utf-8") as f:
            f.write(codigo)
            ruta_temp = f.name
        try:
            calidad = evaluar_calidad(ruta_temp)
        finally:
            try:
                os.remove(ruta_temp)
            except OSError:
                pass

        _emitir("ronda_calidad", ronda=ronda + 1, puntaje=calidad.get("puntaje"))

        hallazgos_serios = calidad.get("detalle", {}).get("seguridad", []) + calidad.get("detalle", {}).get("bugs", [])
        if calidad.get("puntaje", 0) >= puntaje_minimo or not hallazgos_serios:
            break

        system = (
            "Sos SiPi IA. Te dan codigo SiPi que YA FUNCIONA, pero un analisis "
            "estatico encontro hallazgos de seguridad o bugs potenciales. "
            "Resolvé ESPECIFICAMENTE esos hallazgos, sin cambiar el "
            "comportamiento del programa. Devolvé el programa completo en un "
            "unico bloque ```sipi ... ```.\n\n"
            f"Referencia del lenguaje:\n{sipi_ia._referencia_lenguaje_para_prompt()}"
        )
        contenido = (
            f"Codigo:\n```sipi\n{codigo}\n```\n\nHallazgos a resolver:\n"
            + "\n".join(f"- {h}" for h in hallazgos_serios)
        )
        respuesta = sipi_ia._llamar_modelo([{"role": "user", "content": contenido}], system, max_tokens=2000)
        codigo_corregido = sipi_ia._extraer_bloque_codigo(respuesta)

        # el arreglo de calidad no puede romper la ejecucion -- se
        # revalida contra el motor real antes de aceptarlo
        ok, _, _ = sipi_ia._ejecutar_codigo_real(ruta_motor_sipi, codigo_corregido)
        if ok:
            codigo = codigo_corregido
        ronda += 1

    return {
        "codigo": codigo, "exito_ejecucion": True, "calidad": calidad,
        "rondas_calidad": ronda,
    }


# ------- Interfaz de herramienta para otras IAs (comando estructurado JSON) -------

def ejecutar_comando_json(comando):
    """Punto de entrada unico para que cualquier IA/agente externo use
    SiPi IA como herramienta, sin conocer la API interna de Python.
    'comando' es un dict ya parseado (o un string JSON). Devuelve
    siempre un dict serializable a JSON con al menos {"ok": bool}.

    Acciones soportadas:
      - "generar_codigo": {"descripcion": "..."} -> {"ok", "codigo"}
      - "corregir_error": {"codigo": "...", "error": "..."} -> {"ok", "codigo"}
      - "auto_corregir": {"descripcion"?, "codigo"?, "max_intentos"?}
            -> {"ok", "codigo", "intentos", "motivo_fin"}
      - "construir_proyecto": {"objetivo": "...", "carpeta": "...", "restricciones"?}
            -> {"ok", "resultado": ResultadoProyecto.a_dict()}
      - "optimizar_codigo": {"codigo": "..."} -> {"ok", "resultado"}
      - "generar_pruebas": {"codigo": "...", "comportamiento_esperado"?} -> {"ok", "codigo_prueba"}
      - "diagnosticar_error": {"codigo": "...", "error": "..."} -> {"ok", "diagnostico"}
      - "crear_plan": {"objetivo": "...", "restricciones"?} -> {"ok", "plan"}
      - "indexar_proyecto": {"carpeta": "...", "con_resumen_ia"?}
            -> {"ok", "archivos", "contexto_resumido"}
      - "buscar_en_proyecto": {"carpeta": "...", "termino": "..."} -> {"ok", "resultados"}
      - "renombrar_en_proyecto": {"carpeta", "nombre_viejo", "nombre_nuevo", "aplicar"?}
            -> {"ok", "resultado"} (aplicar=false hace un dry-run sin tocar archivos)
      - "verificar_proyecto": {"carpeta": "..."} -> {"ok", "resultados"} (corre todos los .sipi)
      - "sesion_pedir": {"carpeta", "sesion_id"?, "mensaje", "auto_verificar"?, "deshacer"?, "guardar_como"?}
            -> {"ok", "codigo", "turnos"} (conversacion persistente para refinar codigo en varias vueltas)
    """
    if isinstance(comando, str):
        comando = _parsear_json_de_respuesta(comando) or json.loads(comando)

    accion = comando.get("accion")
    try:
        if accion == "generar_codigo":
            codigo = sipi_ia.generar_codigo(comando["descripcion"], comando.get("codigo_actual"))
            return {"ok": True, "codigo": codigo}

        elif accion == "corregir_error":
            resultado = sipi_ia.corregir_error(comando["codigo"], comando["error"])
            return {"ok": True, "codigo": sipi_ia._extraer_bloque_codigo(resultado), "explicacion": resultado}

        elif accion == "auto_corregir":
            ruta_motor = _ruta_motor_sipi()
            resultado = sipi_ia.generar_con_auto_correccion(
                comando.get("descripcion", "un programa SiPi funcional"),
                ruta_motor,
                codigo_inicial=comando.get("codigo"),
                max_intentos=comando.get("max_intentos", 4),
            )
            return {
                "ok": resultado.exito, "codigo": resultado.codigo_final,
                "intentos": len(resultado.intentos), "motivo_fin": resultado.motivo_fin,
            }

        elif accion == "construir_proyecto":
            resultado = construir_proyecto(
                comando["objetivo"], comando["carpeta"],
                restricciones=comando.get("restricciones"),
                max_intentos_por_archivo=comando.get("max_intentos_por_archivo", 4),
            )
            return {"ok": resultado.exito, "resultado": resultado.a_dict()}

        elif accion == "optimizar_codigo":
            return {"ok": True, "resultado": optimizar_codigo(comando["codigo"])}

        elif accion == "generar_pruebas":
            return {"ok": True, "codigo_prueba": generar_pruebas(
                comando["codigo"], comando.get("comportamiento_esperado", ""))}

        elif accion == "diagnosticar_error":
            return {"ok": True, "diagnostico": diagnosticar_y_explicar_error(comando["codigo"], comando["error"])}

        elif accion == "crear_plan":
            return {"ok": True, "plan": crear_plan(comando["objetivo"], comando.get("restricciones"))}

        elif accion == "indexar_proyecto":
            indice = sipi_ia_memoria.IndiceProyecto(comando["carpeta"])
            indice.reconstruir(con_resumen_ia=comando.get("con_resumen_ia", False))
            return {"ok": True, "archivos": indice.archivos, "contexto_resumido": indice.contexto_resumido()}

        elif accion == "buscar_en_proyecto":
            indice = sipi_ia_memoria.IndiceProyecto(comando["carpeta"])
            indice.reconstruir(con_resumen_ia=False)
            return {"ok": True, "resultados": indice.buscar(comando["termino"])}

        elif accion == "renombrar_en_proyecto":
            resultado = sipi_ia_memoria.renombrar_en_proyecto(
                comando["carpeta"], comando["nombre_viejo"], comando["nombre_nuevo"],
                aplicar=comando.get("aplicar", False),
            )
            return {"ok": True, "resultado": resultado}

        elif accion == "verificar_proyecto":
            ruta_motor = _ruta_motor_sipi()
            resultados = sipi_ia_memoria.verificar_proyecto_completo(comando["carpeta"], ruta_motor)
            return {"ok": all(r["ok"] for r in resultados), "resultados": resultados}

        elif accion == "sesion_pedir":
            return _accion_sesion_pedir(comando)

        elif accion == "buscar_patrones":
            resultados = sipi_ia_biblioteca.buscar_similares(
                comando["descripcion"], top_n=comando.get("top_n", 3),
                umbral_minimo=comando.get("umbral_minimo", 0.25),
            )
            return {"ok": True, "resultados": resultados}

        elif accion == "estadisticas_biblioteca":
            return {"ok": True, "estadisticas": sipi_ia_biblioteca.estadisticas()}

        elif accion == "evaluar_calidad":
            return {"ok": True, "resultado": evaluar_calidad(comando["ruta_archivo"])}

        elif accion == "generar_con_calidad":
            ruta_motor = _ruta_motor_sipi()
            resultado = generar_con_calidad_garantizada(
                comando["descripcion"], ruta_motor, codigo_inicial=comando.get("codigo"),
                puntaje_minimo=comando.get("puntaje_minimo", 70),
            )
            return {"ok": resultado["exito_ejecucion"], "resultado": resultado}

        else:
            return {"ok": False, "error": f"Accion desconocida: {accion!r}. Ver el docstring de ejecutar_comando_json."}

    except sipi_ia.ErrorSiPiIA as e:
        return {"ok": False, "error": str(e)}
    except KeyError as e:
        return {"ok": False, "error": f"Falta el campo requerido {e} para la accion {accion!r}."}
    except Exception as e:  # noqa: BLE001 - esto es una frontera de herramienta externa: nunca debe tirar traceback crudo a otro proceso
        return {"ok": False, "error": f"Error inesperado en SiPi IA Agente: {e}"}


def _ruta_sesion(carpeta, sesion_id):
    return os.path.join(carpeta, f".sipi_sesion_{sesion_id}.json")


def _accion_sesion_pedir(comando):
    """Las sesiones conversacionales (sipi_ia.SesionIA) viven en memoria
    dentro de un proceso Python -- pero como cada llamada por CLI/JSON
    es un proceso nuevo, se persisten a disco entre pedidos para que
    una IA externa pueda mantener una conversacion de refinamiento a
    lo largo de varias invocaciones separadas, cada una un comando JSON
    independiente. El archivo de sesion vive dentro de la carpeta del
    proyecto, con el mismo criterio que el indice."""
    carpeta = comando["carpeta"]
    sesion_id = comando.get("sesion_id", "default")
    os.makedirs(carpeta, exist_ok=True)
    ruta = _ruta_sesion(carpeta, sesion_id)

    ruta_motor = None
    try:
        ruta_motor = _ruta_motor_sipi()
    except sipi_ia.ErrorSiPiIA:
        pass

    if os.path.exists(ruta):
        with open(ruta, "r", encoding="utf-8") as f:
            sesion = sipi_ia.SesionIA.desde_exportado(json.load(f), ruta_motor_sipi=ruta_motor)
    else:
        sesion = sipi_ia.SesionIA(ruta_motor_sipi=ruta_motor)

    if comando.get("deshacer"):
        codigo = sesion.deshacer()
    else:
        codigo = sesion.pedir(comando["mensaje"], auto_verificar=comando.get("auto_verificar", False))

    with open(ruta, "w", encoding="utf-8") as f:
        json.dump(sesion.exportar(), f, ensure_ascii=False, indent=2)

    if comando.get("guardar_como"):
        ruta_archivo = os.path.join(carpeta, comando["guardar_como"])
        with open(ruta_archivo, "w", encoding="utf-8") as f:
            f.write(codigo or "")

    return {
        "ok": codigo is not None, "codigo": codigo, "sesion_id": sesion_id,
        "turnos": len(sesion.historial_codigo),
    }


def main():
    parser = argparse.ArgumentParser(
        description="SiPi IA Agente: herramienta de linea de comandos para que cualquier IA "
                    "construya software en SiPi (generacion, auto-correccion, proyectos multi-archivo)."
    )
    parser.add_argument("--json", metavar="COMANDO_JSON",
                         help="Comando estructurado en JSON. Ver ejecutar_comando_json() para el formato.")
    parser.add_argument("--objetivo", help="Atajo: equivalente a --json con accion 'construir_proyecto'.")
    parser.add_argument("--carpeta", default="./proyecto_sipi", help="Carpeta destino para --objetivo.")
    args = parser.parse_args()

    if args.json:
        resultado = ejecutar_comando_json(args.json)
    elif args.objetivo:
        resultado = ejecutar_comando_json({
            "accion": "construir_proyecto", "objetivo": args.objetivo, "carpeta": args.carpeta,
        })
    else:
        parser.print_help()
        sys.exit(1)

    print(json.dumps(resultado, ensure_ascii=False, indent=2))
    sys.exit(0 if resultado.get("ok") else 1)


if __name__ == "__main__":
    main()
