"""
@RAG_CONTEXT
{
  "archivo": "src/ocr_indexer/searcher.py",
  "lineas": 243,
  "tokens_estimados": 2317,
  "hash_contenido": "3f94f1270ddb0ea6ef30cf5b9fdc810390808745341282dac4d0587febbcea1c",
  "creacion": "2026-03-13",
  "modificacion": "2026-03-17",
  "tags": [
    "os",
    "re",
    "sys",
    "json"
  ],
  "descripcion_corta": "Motor de búsqueda con índice invertido para OCR",
  "descripcion_larga": "Implementa un motor de búsqueda de texto con índice invertido que procesa documentos OCR. Construye un índice tokenizado con normalización por tolerancias (reemplazos de caracteres), soporta búsquedas booleanas con grupos OR/AND, y resalta múltiples coincidencias en los resultados. Incluye caché del índice invertido en disco con validación de frescura basada en timestamps."
}
"""
#!/usr/bin/env python3
import sys
import json
import os
import re

GREEN = '\033[0;32m'
BLUE = '\033[0;34m'
YELLOW = '\033[1;33m'
RED = '\033[0;31m'
NC = '\033[0m'

def aplicar_tolerancias(texto, tolerancias):
    for orig, reemp in tolerancias.items():
        texto = texto.replace(orig, reemp)
    return texto

def resaltar_multiples(snippet, terminos, tolerancias):
    """Pinta de verde TODAS las palabras que coincidan en el recorte"""
    snippet_norm = aplicar_tolerancias(snippet.lower(), tolerancias)
    matches = []

    for t in terminos:
        t_n = aplicar_tolerancias(t.lower(), tolerancias)
        if not t_n: continue
        start = 0
        while True:
            idx = snippet_norm.find(t_n, start)
            if idx == -1: break
            matches.append((idx, idx + len(t_n)))
            start = idx + len(t_n)

    if not matches: return snippet

    # Limpiar empalmes para no corromper los colores
    matches.sort()
    filtered = []
    last_end = -1
    for start, end in matches:
        if start >= last_end:
            filtered.append((start, end))
            last_end = end

    # Reconstruir el texto con los colores
    res = ""
    last_idx = 0
    for start, end in filtered:
        res += snippet[last_idx:start]
        res += f"{GREEN}{snippet[start:end]}{NC}{YELLOW}"
        last_idx = end
    res += snippet[last_idx:]
    return res


def construir_invertido(db, tolerancias, min_len=3):
    """Construye índice invertido: token_normalizado → [filepath, ...]"""
    invertido = {}
    for filepath, data in db.items():
        if not isinstance(data, dict) or data.get("estado") == "pendiente":
            continue
        texto = data.get("contenido", {}).get("texto_ocr", "") or ""
        nombre = data.get("archivo", {}).get("nombre", "") or ""
        combinado = aplicar_tolerancias(texto + " " + nombre, tolerancias).lower()
        for token in re.split(r'\W+', combinado):
            if len(token) >= min_len:
                if token not in invertido:
                    invertido[token] = []
                if filepath not in invertido[token]:
                    invertido[token].append(filepath)
    return invertido


def cargar_o_reconstruir_invertido(index_path, inverted_path, db, tolerancias):
    """Carga inverted.json si está vigente; reconstruye si index.json es más nuevo."""
    if os.path.exists(inverted_path):
        if os.path.getmtime(index_path) <= os.path.getmtime(inverted_path):
            try:
                with open(inverted_path, 'r') as f:
                    return json.load(f)
            except Exception:
                pass  # Corrupto → reconstruir

    invertido = construir_invertido(db, tolerancias)
    try:
        with open(inverted_path, 'w') as f:
            json.dump(invertido, f)
    except Exception:
        pass  # Si no se puede guardar, operar en memoria igual
    return invertido


def buscar_candidatos(or_groups, invertido, tolerancias):
    """Retorna el set de filepaths candidatos usando el índice invertido."""
    candidatos = set()
    for and_group in or_groups:
        grupo_candidatos = None
        for termino in and_group:
            termino_norm = aplicar_tolerancias(termino, tolerancias).lower()
            filepaths = set(invertido.get(termino_norm, []))
            if grupo_candidatos is None:
                grupo_candidatos = filepaths
            else:
                grupo_candidatos &= filepaths
            if not grupo_candidatos:
                break
        if grupo_candidatos:
            candidatos |= grupo_candidatos
    return candidatos


def main():
    if len(sys.argv) < 2:
        print(f"{RED}Error interno: Falta la ruta del índice.{NC}")
        sys.exit(1)

    index_path = sys.argv[1]
    engine_dir = os.path.dirname(index_path)
    config_path = os.path.join(engine_dir, 'config.json')
    inverted_path = os.path.join(engine_dir, 'inverted.json')

    try:
        with open(index_path, 'r') as f: db = json.load(f)
    except:
        print(f"{RED}Error al leer el índice.{NC}")
        sys.exit(1)

    try:
        with open(config_path, 'r') as f: config = json.load(f)
        tolerancias = config.get("tolerancias", {"O": "0", "0": "O"})
    except:
        tolerancias = {"O": "0", "0": "O"}

    invertido = cargar_o_reconstruir_invertido(index_path, inverted_path, db, tolerancias)

    while True:
        print(f"\n{BLUE}===================================================={NC}")
        print(f"{YELLOW}                 BUSCADOR OCR LOCAL                 {NC}")
        print(f"{BLUE}===================================================={NC}")

        print(f"{GREEN}💡 Tip: Usa && (AND) y || (OR). Ej: Razo && 7064 || Lona{NC}")
        entrada_original = input(f"{YELLOW}Búsqueda avanzada (o 'salir'): {NC}").strip()

        if not entrada_original or entrada_original.lower() == 'salir':
            print("Saliendo del buscador...")
            break

        # Reemplazar la coma por OR para mantener compatibilidad
        query_parsed = entrada_original.replace(',', '||')

        # Procesamiento Booleano
        or_groups_raw = query_parsed.split('||')
        or_groups = []
        for og in or_groups_raw:
            and_terms = [t.strip() for t in og.split('&&') if t.strip()]
            if and_terms:
                or_groups.append(and_terms)

        if not or_groups: continue

        # Fase 1: Pre-filtrado O(1) con índice invertido
        candidatos = buscar_candidatos(or_groups, invertido, tolerancias)

        # Fase 2: Verificación booleana exacta + extracción de snippets
        resultados = []

        for filepath in candidatos:
            data = db.get(filepath)
            if not isinstance(data, dict) or data.get("estado") == "pendiente": continue

            contenido = data.get("contenido", {})
            texto_original = contenido.get("texto_ocr", "") if isinstance(contenido, dict) else ""
            archivo_info = data.get("archivo", {})
            nombre_original = archivo_info.get("nombre", "") if isinstance(archivo_info, dict) else ""

            texto_con_tol = aplicar_tolerancias(texto_original, tolerancias)
            nombre_con_tol = aplicar_tolerancias(nombre_original, tolerancias)
            texto_norm = texto_con_tol.lower()
            nombre_norm = nombre_con_tol.lower()

            match_found = False
            terminos_exitosos = []
            termino_para_centro = ""

            for and_group in or_groups:
                group_match = True

                for termino in and_group:
                    termino_con_tol = aplicar_tolerancias(termino, tolerancias)
                    termino_norm = termino_con_tol.lower()

                    if termino_norm not in texto_norm and termino_norm not in nombre_norm:
                        group_match = False
                        break

                if group_match:
                    match_found = True
                    terminos_exitosos = and_group
                    termino_para_centro = and_group[0]
                    break

            if match_found:
                termino_centro_norm = aplicar_tolerancias(termino_para_centro.lower(), tolerancias)
                idx = texto_norm.find(termino_centro_norm)

                if idx != -1:
                    # Lente ampliado a 80 caracteres hacia atrás y hacia adelante
                    inicio = max(0, idx - 80)
                    fin = min(len(texto_original), idx + len(termino_centro_norm) + 80)
                    snippet = texto_original[inicio:fin].replace('\n', ' ')

                    # Usamos la nueva función para pintar TODAS las palabras
                    snippet_resaltado = resaltar_multiples(snippet, terminos_exitosos, tolerancias)
                    snippet_final = f"...{snippet_resaltado}..."
                else:
                    snippet_final = "Coincidencia hallada en el nombre del archivo."

                ext = archivo_info.get('extension', '') if isinstance(archivo_info, dict) else ''
                resultados.append({
                    "archivo": f"{nombre_original}.{ext}",
                    "ruta": filepath,
                    "terminos_and": ", ".join(terminos_exitosos),
                    "snippet": snippet_final
                })

        print(f"\n{BLUE}----------------------------------------------------{NC}")
        info_tol = f"({len(tolerancias)} reglas)" if tolerancias else ""
        print(f"Resultados para: {GREEN}'{entrada_original}'{NC} {YELLOW}{info_tol}{NC}")
        print(f"Se encontraron {YELLOW}{len(resultados)}{NC} coincidencias.\n")

        for i, res in enumerate(resultados, 1):
            print(f"{BLUE}[{i}] {res['archivo']}{NC}")
            print(f"    {GREEN}Ruta:{NC}       {res['ruta']}")
            print(f"    {YELLOW}Motivo:{NC}     Se encontraron: [{GREEN}{res['terminos_and']}{NC}]")
            print(f"    {YELLOW}Texto:{NC}      {res['snippet']}\n")

        print(f"{BLUE}===================================================={NC}")
        resp = input(f"{YELLOW}¿Qué deseas hacer? [Enter] Otra Búsqueda | [M] Menú | [S] Salir: {NC}").strip().lower()

        if resp == 's': sys.exit(0)
        elif resp == 'm': break

if __name__ == "__main__":
    main()
