#!/usr/bin/env python3
"""
sipi_ia_biblioteca.py - Biblioteca de patrones para SiPi IA.

Problema que resuelve: cada vez que se le pide a la IA "un programa que
valide un DNI" o "un menu con opciones numeradas", si ya se resolvio
algo parecido antes, generarlo de cero otra vez es mas lento, mas caro,
y puede salir con un estilo distinto al de la vez anterior. Esta
biblioteca guarda soluciones que YA SE VERIFICARON que funcionan (con
generar_con_auto_correccion, exito=True) y las indexa por palabras
clave, para que un pedido nuevo primero busque si hay algo parecido
antes de generar desde cero.

No usa embeddings ni ninguna libreria de ML -- el indexado es por
palabras clave normalizadas (minusculas, sin tildes, sin palabras
vacias), lo cual alcanza sobradamente para este caso de uso (pedidos
cortos en español, vocabulario acotado del dominio) y no agrega
ninguna dependencia externa.

Flujo tipico dentro de sipi_ia_agente.py:

    1. Antes de generar_con_auto_correccion desde cero, se busca en la
       biblioteca con buscar_similares(descripcion).
    2. Si hay una coincidencia fuerte, se usa esa solucion como
       'codigo_inicial' y se le pide a la IA que la ADAPTE al pedido
       nuevo (mas rapido y mas barato que generar de cero, y mantiene
       consistencia de estilo entre programas parecidos).
    3. Cuando un programa nuevo se verifica con exito, se guarda en la
       biblioteca con guardar_solucion() para que la proxima vez que
       aparezca algo parecido ya este disponible.

La biblioteca es compartida (vive en la config de usuario, no en un
proyecto puntual) porque el objetivo es que el conocimiento se acumule
entre proyectos distintos, no que se pierda cada vez.
"""
import os
import re
import json
import time
import hashlib

PALABRAS_VACIAS = {
    "un", "una", "el", "la", "los", "las", "de", "del", "que", "para", "con",
    "por", "en", "y", "o", "a", "al", "se", "su", "sus", "es", "sea", "sean",
    "como", "programa", "sipi",
}


def _carpeta_config_usuario():
    if os.name == "nt":
        base = os.environ.get("APPDATA") or os.path.expanduser("~")
    else:
        base = os.environ.get("XDG_CONFIG_HOME") or os.path.join(os.path.expanduser("~"), ".config")
    carpeta = os.path.join(base, "sipi")
    try:
        os.makedirs(carpeta, exist_ok=True)
    except OSError:
        pass
    return carpeta


ARCHIVO_BIBLIOTECA = os.path.join(_carpeta_config_usuario(), "sipi_ia_biblioteca.json")
MAX_SOLUCIONES_GUARDADAS = 300  # tope simple para que el archivo no crezca sin limite


def _normalizar(texto):
    """minusculas, sin tildes, sin puntuacion -- para que 'validación'
    y 'validacion' matcheen igual, sin depender de ninguna libreria de
    normalizacion de texto."""
    texto = texto.lower()
    reemplazos = {"á": "a", "é": "e", "í": "i", "ó": "o", "ú": "u", "ñ": "n"}
    for viejo, nuevo in reemplazos.items():
        texto = texto.replace(viejo, nuevo)
    texto = re.sub(r"[^a-z0-9\s]", " ", texto)
    return texto


def _palabras_clave(texto):
    """Ademas de normalizar, aplica un 'stemming' muy simple (truncar a
    los primeros 5 caracteres de cada palabra de mas de 5 letras) para
    que conjugaciones distintas de una misma raiz matcheen entre si
    (validar/valida/validando/validacion todas quedan en 'valid') sin
    necesidad de un stemmer real para español."""
    normalizado = _normalizar(texto)
    palabras = {p for p in normalizado.split() if p and p not in PALABRAS_VACIAS and len(p) > 2}
    return {(p[:5] if len(p) > 5 else p) for p in palabras}


def _cargar():
    if os.path.exists(ARCHIVO_BIBLIOTECA):
        try:
            with open(ARCHIVO_BIBLIOTECA, "r", encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError):
            return {"soluciones": []}
    return {"soluciones": []}


def _guardar(datos):
    try:
        with open(ARCHIVO_BIBLIOTECA, "w", encoding="utf-8") as f:
            json.dump(datos, f, ensure_ascii=False, indent=2)
    except OSError:
        pass


def guardar_solucion(descripcion, codigo, etiquetas=None):
    """Guarda una solucion ya verificada. Si ya existe una solucion muy
    parecida (mismo hash de palabras clave), la actualiza en vez de
    duplicarla -- asi la biblioteca converge a la MEJOR/ultima version
    de cada patron en vez de acumular variantes redundantes."""
    datos = _cargar()
    claves = sorted(_palabras_clave(descripcion))
    huella = hashlib.sha256(" ".join(claves).encode("utf-8")).hexdigest()[:16]

    entrada = {
        "huella": huella,
        "descripcion": descripcion,
        "palabras_clave": claves,
        "codigo": codigo,
        "etiquetas": etiquetas or [],
        "usos": 0,
        "guardado_en": time.time(),
    }

    existentes = datos["soluciones"]
    for i, sol in enumerate(existentes):
        if sol["huella"] == huella:
            entrada["usos"] = sol.get("usos", 0)  # conserva el contador de uso de la version anterior
            existentes[i] = entrada
            _guardar(datos)
            return entrada

    existentes.append(entrada)
    if len(existentes) > MAX_SOLUCIONES_GUARDADAS:
        # descarta las menos usadas y mas viejas primero, para quedarse
        # con lo que de verdad demostro ser util
        existentes.sort(key=lambda s: (s.get("usos", 0), s.get("guardado_en", 0)))
        del existentes[: len(existentes) - MAX_SOLUCIONES_GUARDADAS]
    _guardar(datos)
    return entrada


def buscar_similares(descripcion, top_n=3, umbral_minimo=0.25):
    """Devuelve hasta top_n soluciones guardadas ordenadas por similitud
    de palabras clave (coeficiente de Jaccard: interseccion / union).
    umbral_minimo filtra matches demasiado debiles -- mejor no sugerir
    nada que sugerir algo irrelevante que confunda a la IA al adaptar."""
    datos = _cargar()
    claves_pedido = _palabras_clave(descripcion)
    if not claves_pedido:
        return []

    puntuados = []
    for sol in datos["soluciones"]:
        claves_sol = set(sol["palabras_clave"])
        if not claves_sol:
            continue
        interseccion = claves_pedido & claves_sol
        union = claves_pedido | claves_sol
        similitud = len(interseccion) / len(union) if union else 0
        if similitud >= umbral_minimo:
            puntuados.append((similitud, sol))

    puntuados.sort(key=lambda par: par[0], reverse=True)
    resultado = []
    for similitud, sol in puntuados[:top_n]:
        resultado.append({
            "similitud": round(similitud, 2),
            "descripcion": sol["descripcion"],
            "codigo": sol["codigo"],
            "etiquetas": sol.get("etiquetas", []),
        })
    return resultado


def registrar_uso(descripcion):
    """Incrementa el contador de usos de la solucion que mas se parece
    a 'descripcion' -- asi con el tiempo la biblioteca sabe que
    patrones son de verdad utiles (y sobreviven la poda de
    MAX_SOLUCIONES_GUARDADAS) en vez de solo lo mas reciente."""
    datos = _cargar()
    claves_pedido = _palabras_clave(descripcion)
    mejor = None
    mejor_similitud = 0
    for sol in datos["soluciones"]:
        claves_sol = set(sol["palabras_clave"])
        union = claves_pedido | claves_sol
        similitud = len(claves_pedido & claves_sol) / len(union) if union else 0
        if similitud > mejor_similitud:
            mejor_similitud = similitud
            mejor = sol
    if mejor and mejor_similitud > 0.5:
        mejor["usos"] = mejor.get("usos", 0) + 1
        _guardar(datos)


def estadisticas():
    datos = _cargar()
    soluciones = datos["soluciones"]
    return {
        "total_soluciones": len(soluciones),
        "mas_usadas": sorted(
            [{"descripcion": s["descripcion"], "usos": s.get("usos", 0)} for s in soluciones],
            key=lambda s: s["usos"], reverse=True,
        )[:10],
    }


def vaciar_biblioteca():
    _guardar({"soluciones": []})
