#!/usr/bin/env python3
"""Construye el indice candidato uniendo lo viejo, lo cosechado y lo sintetizado.

    ./reindexa.py            escribe datos/saber.jsonl.nuevo (NO toca el vivo)

## Que une

  1. El indice vivo (datos/saber.jsonl): comandos de Linux, glosario, fichas,
     servidores... todo lo que cosecha no produce. Se conserva entero.
  2. cosecha.jsonl: la metodologia de pentesting que faltaba.
  3. sintesis.jsonl: preguntas en castellano que apuntan a esos fragmentos.

## Indexacion multi-representacion

Cada entrada se EMBEBE por su campo `indexar` si lo tiene (las preguntas
sintetizadas), y si no por `texto` (todo lo demas). Asi una entrada de pregunta
se compara con la consulta del usuario pregunta-contra-pregunta, pero lo que se
le muestra al cerebro sigue siendo el fragmento literal de `texto`. busca.py no
cambia: solo lee `texto` y `v`.

## No destruye nada

Escribe a `saber.jsonl.nuevo`. Quien decide si sustituye al vivo es evalua.py +
correr_noche.sh, y solo si mide que es mejor, guardando antes una copia.

## Deduplicado

Por los primeros 120 caracteres del texto normalizado (para las entradas
normales) o de la pregunta (para las sintetizadas). OSCP_APUNTES duplica apuntes
de primer nivel; esto los colapsa.
"""
import json
import os
import re
import sys
import unicodedata

AQUI = os.path.dirname(os.path.abspath(__file__))
RAIZ = os.path.dirname(os.path.dirname(AQUI))
DATOS = os.path.join(RAIZ, "datos")
# La base son las entradas que cosecha NO produce (comandos Linux, glosario,
# fichas). Por defecto el indice vivo, pero se puede fijar con SABER_BASE al
# indice ORIGINAL para una reconstruccion limpia que no apile sintesis sobre
# sintesis y no infle el indice a cada pasada.
VIVO = os.environ.get("SABER_BASE") or os.path.join(DATOS, "saber.jsonl")
COSECHA = os.path.join(DATOS, "cosecha.jsonl")
SINTESIS = os.path.join(DATOS, "sintesis.jsonl")
NUEVO = os.path.join(DATOS, "saber.jsonl.nuevo")

MODELO = "minishlab/potion-multilingual-128M"


def _norm(t):
    t = unicodedata.normalize("NFD", (t or "").lower())
    t = "".join(c for c in t if unicodedata.category(c) != "Mn")
    return re.sub(r"\s+", " ", re.sub(r"[^a-z0-9 ]", " ", t)).strip()


def _lee(ruta, quita_v=False):
    if not os.path.exists(ruta):
        return
    with open(ruta, encoding="utf-8") as f:
        for l in f:
            try:
                d = json.loads(l)
            except json.JSONDecodeError:
                continue
            if quita_v:
                d.pop("v", None)
            yield d


def main():
    entradas, vistos = [], set()
    fuentes = [("vivo", VIVO, True), ("cosecha", COSECHA, False),
               ("sintesis", SINTESIS, False)]
    cuenta = {}
    for nombre, ruta, quita in fuentes:
        n = 0
        for d in _lee(ruta, quita_v=quita):
            clave_txt = d.get("indexar") or d.get("texto", "")
            firma = _norm(clave_txt)[:120]
            if not firma or firma in vistos:
                continue
            vistos.add(firma)
            entradas.append(d)
            n += 1
        cuenta[nombre] = n
        print(f"  {nombre:9s} {n:6d} entradas nuevas", flush=True)

    print(f"\n  total tras deduplicar: {len(entradas)}", flush=True)

    print("  cargando el modelo de embeddings...", flush=True)
    from model2vec import StaticModel
    modelo = StaticModel.from_pretrained(MODELO)

    # se embebe `indexar` si existe (la pregunta), si no `texto`
    a_embeber = [(e.get("indexar") or e["texto"]) for e in entradas]
    print(f"  calculando {len(a_embeber)} vectores...", flush=True)
    vectores = modelo.encode(a_embeber, show_progress_bar=False)

    with open(NUEVO, "w", encoding="utf-8") as f:
        for e, v in zip(entradas, vectores):
            e["v"] = [round(float(x), 5) for x in v]
            f.write(json.dumps(e, ensure_ascii=False) + "\n")
    print(f"  escrito: {NUEVO} ({os.path.getsize(NUEVO)//1024} KB)", flush=True)
    print(f"  RESUMEN vivo={cuenta['vivo']} cosecha={cuenta['cosecha']} "
          f"sintesis={cuenta['sintesis']} total={len(entradas)}", flush=True)
    return 0


if __name__ == "__main__":
    sys.exit(main())
