#!/usr/bin/env python3
"""
sipi_ia_memoria.py - Memoria e indice de proyecto para SiPi IA.

Problema que resuelve: cuando un proyecto tiene varios archivos (los
que arma sipi_ia_agente.py, o cualquier carpeta de programas SiPi que
ya exista), mandarle TODO el contenido de TODOS los archivos al modelo
en cada pedido es caro, lento, y en proyectos grandes directamente no
entra en el contexto. Este modulo arma un indice liviano del proyecto
-- un resumen de que hace cada archivo y sus funciones/variables
principales -- para poder darle a la IA solo el contexto relevante a
cada pedido, no el proyecto entero.

Tambien expone busqueda por contenido (para preguntas como "en que
archivo esta la funcion que calcula el descuento?") y una herramienta
de refactor: renombrar algo de forma consistente en todos los archivos
del proyecto a la vez, algo que un solo archivo aislado no puede
resolver.

No depende de sipi_ia.py para la indexacion basica (funciona con
analisis de texto simple, instantaneo, sin llamar a la API) -- solo
usa el modelo para el resumen en lenguaje natural de cada archivo,
que se cachea en disco para no repetir el gasto si el archivo no
cambio.
"""
import os
import re
import json
import hashlib

import sipi_ia

NOMBRE_ARCHIVO_INDICE = ".sipi_indice.json"

# Patrones simples para extraer la "forma" de un archivo SiPi sin tener
# que parsearlo de verdad -- alcanza para armar un mapa de referencia
# rapido (que funciones y variables globales tiene cada archivo).
PATRON_FUNCION = re.compile(r'^\s*funcion\s+(\w+)\s*\(([^)]*)\)', re.MULTILINE)
PATRON_CLASE = re.compile(r'^\s*clase\s+(\w+)', re.MULTILINE)
PATRON_VARIABLE_GLOBAL = re.compile(r'^variable\s+(\w+)\s*=', re.MULTILINE)
PATRON_PROGRAMA = re.compile(r'^\s*programa\s+"([^"]*)"', re.MULTILINE)


def _hash_contenido(texto):
    return hashlib.sha256(texto.encode("utf-8")).hexdigest()[:16]


def _analizar_estructura(codigo):
    """Analisis sintactico superficial (regex, sin dependencias) para
    sacar un mapa rapido de que define un archivo, sin gastar una
    llamada a la API para algo que se puede leer directo del texto."""
    return {
        "titulo_programa": (PATRON_PROGRAMA.search(codigo).group(1) if PATRON_PROGRAMA.search(codigo) else None),
        "funciones": [
            {"nombre": m.group(1), "parametros": [p.strip() for p in m.group(2).split(",") if p.strip()]}
            for m in PATRON_FUNCION.finditer(codigo)
        ],
        "clases": [m.group(1) for m in PATRON_CLASE.finditer(codigo)],
        "variables_globales": list(dict.fromkeys(m.group(1) for m in PATRON_VARIABLE_GLOBAL.finditer(codigo))),
        "lineas": codigo.count("\n") + 1,
    }


class IndiceProyecto:
    """Indice de un directorio de archivos .sipi. Se puede reconstruir
    barato (sin API) para la estructura, y opcionalmente enriquecer con
    un resumen en lenguaje natural por archivo (con cache en disco, solo
    se re-genera si el archivo cambio de verdad)."""

    def __init__(self, carpeta):
        self.carpeta = os.path.abspath(carpeta)
        self.archivos = {}  # nombre -> {estructura, resumen, hash}
        self._ruta_cache = os.path.join(self.carpeta, NOMBRE_ARCHIVO_INDICE)

    def _cargar_cache(self):
        if os.path.exists(self._ruta_cache):
            try:
                with open(self._ruta_cache, "r", encoding="utf-8") as f:
                    return json.load(f)
            except (json.JSONDecodeError, OSError):
                return {}
        return {}

    def _guardar_cache(self):
        try:
            with open(self._ruta_cache, "w", encoding="utf-8") as f:
                json.dump(self.archivos, f, ensure_ascii=False, indent=2)
        except OSError:
            pass

    def reconstruir(self, con_resumen_ia=False):
        """Recorre todos los .sipi de la carpeta (no recursivo, un
        proyecto de SiPi vive en una carpeta plana) y arma/actualiza el
        indice. Si con_resumen_ia=True, ademas le pide al modelo un
        resumen de una linea por archivo -- solo para los archivos
        nuevos o modificados desde la ultima vez (por hash de
        contenido), para no re-gastar en los que no cambiaron."""
        cache_previo = self._cargar_cache()
        archivos_sipi = sorted(f for f in os.listdir(self.carpeta) if f.endswith(".sipi"))

        for nombre in archivos_sipi:
            ruta = os.path.join(self.carpeta, nombre)
            try:
                with open(ruta, "r", encoding="utf-8") as f:
                    codigo = f.read()
            except OSError:
                continue

            hash_actual = _hash_contenido(codigo)
            entrada_previa = cache_previo.get(nombre)
            sin_cambios = entrada_previa and entrada_previa.get("hash") == hash_actual

            estructura = entrada_previa["estructura"] if sin_cambios else _analizar_estructura(codigo)
            resumen = entrada_previa.get("resumen") if sin_cambios else None

            if con_resumen_ia and not resumen:
                try:
                    resumen = self._resumir_archivo(codigo)
                except sipi_ia.ErrorSiPiIA:
                    resumen = None

            self.archivos[nombre] = {
                "hash": hash_actual, "estructura": estructura, "resumen": resumen,
            }

        # limpia del indice los archivos que ya no existen en la carpeta
        self.archivos = {k: v for k, v in self.archivos.items() if k in archivos_sipi}
        self._guardar_cache()
        return self.archivos

    def _resumir_archivo(self, codigo):
        system = (
            "Resumi en UNA sola oracion corta, en español, que hace este programa SiPi. "
            "Sin preambulo, sin comillas, solo la oracion."
        )
        return sipi_ia._llamar_modelo(
            [{"role": "user", "content": f"```sipi\n{codigo}\n```"}], system, max_tokens=100
        ).strip()

    def contexto_resumido(self):
        """Version compacta del indice para pasarle a un prompt: mucho
        mas liviano que mandar el codigo completo de todos los
        archivos, pero suficiente para que el modelo sepa que hay en
        el proyecto antes de tocar un archivo puntual."""
        lineas = []
        for nombre, info in self.archivos.items():
            est = info["estructura"]
            partes = [nombre]
            if info.get("resumen"):
                partes.append(f"- {info['resumen']}")
            funciones = ", ".join(f["nombre"] for f in est["funciones"])
            if funciones:
                partes.append(f"(funciones: {funciones})")
            lineas.append(" ".join(partes))
        return "\n".join(lineas)

    def buscar(self, termino):
        """Busca un termino (nombre de funcion, variable, clase, o
        texto libre) en el indice y en el contenido de los archivos.
        Devuelve una lista de coincidencias con el archivo y la razon
        del match, para responder preguntas tipo 'donde esta X' sin
        tener que mandarle todo el proyecto al modelo."""
        termino_bajo = termino.lower()
        resultados = []
        for nombre, info in self.archivos.items():
            est = info["estructura"]
            for func in est["funciones"]:
                if termino_bajo in func["nombre"].lower():
                    resultados.append({"archivo": nombre, "tipo": "funcion", "nombre": func["nombre"]})
            for clase in est["clases"]:
                if termino_bajo in clase.lower():
                    resultados.append({"archivo": nombre, "tipo": "clase", "nombre": clase})
            for var in est["variables_globales"]:
                if termino_bajo in var.lower():
                    resultados.append({"archivo": nombre, "tipo": "variable_global", "nombre": var})
            if info.get("resumen") and termino_bajo in info["resumen"].lower():
                resultados.append({"archivo": nombre, "tipo": "resumen", "nombre": info["resumen"]})

            ruta = os.path.join(self.carpeta, nombre)
            try:
                with open(ruta, "r", encoding="utf-8") as f:
                    contenido = f.read()
                if termino_bajo in contenido.lower() and not any(
                    r["archivo"] == nombre for r in resultados
                ):
                    linea_num = next(
                        (i + 1 for i, linea in enumerate(contenido.splitlines()) if termino_bajo in linea.lower()),
                        None,
                    )
                    resultados.append({"archivo": nombre, "tipo": "texto", "nombre": f"linea {linea_num}"})
            except OSError:
                pass
        return resultados


# ------- Refactor entre archivos -------

def renombrar_en_proyecto(carpeta, nombre_viejo, nombre_nuevo, aplicar=False):
    """Renombra de forma consistente una variable/funcion en TODOS los
    archivos .sipi del proyecto. Usa reemplazo de palabra completa
    (con limites de palabra, para no romper 'total' al renombrar 'tot'
    dentro de otra palabra). Con aplicar=False solo hace un
    'dry run' y devuelve la vista previa de cambios, sin tocar los
    archivos -- pensado para que una IA (o una persona) pueda revisar
    el alcance del cambio antes de confirmarlo."""
    patron = re.compile(r'\b' + re.escape(nombre_viejo) + r'\b')
    cambios = []
    for nombre in sorted(f for f in os.listdir(carpeta) if f.endswith(".sipi")):
        ruta = os.path.join(carpeta, nombre)
        with open(ruta, "r", encoding="utf-8") as f:
            contenido = f.read()
        ocurrencias = len(patron.findall(contenido))
        if ocurrencias == 0:
            continue
        nuevo_contenido = patron.sub(nombre_nuevo, contenido)
        cambios.append({"archivo": nombre, "ocurrencias": ocurrencias})
        if aplicar:
            with open(ruta, "w", encoding="utf-8") as f:
                f.write(nuevo_contenido)
    return {
        "aplicado": aplicar, "nombre_viejo": nombre_viejo, "nombre_nuevo": nombre_nuevo,
        "archivos_afectados": cambios, "total_ocurrencias": sum(c["ocurrencias"] for c in cambios),
    }


def verificar_proyecto_completo(carpeta, ruta_motor_sipi):
    """Corre TODOS los .sipi de una carpeta contra el motor real y
    devuelve el estado de cada uno -- para saber de un vistazo si un
    refactor o un cambio grande rompio algo en el proyecto, sin tener
    que abrir archivo por archivo."""
    resultados = []
    for nombre in sorted(f for f in os.listdir(carpeta) if f.endswith(".sipi")):
        ruta = os.path.join(carpeta, nombre)
        with open(ruta, "r", encoding="utf-8") as f:
            codigo = f.read()
        ok, salida, error = sipi_ia._ejecutar_codigo_real(ruta_motor_sipi, codigo)
        resultados.append({"archivo": nombre, "ok": ok, "error": error if not ok else None})
    return resultados
