#!/usr/bin/env python3
"""Genera preguntas en castellano para cada fragmento cosechado.

    ./sintetiza.py             procesa datos/cosecha.jsonl (reanudable, horas)
    ./sintetiza.py --muestra 5 ensena 5 y para

## Que problema resuelve, y por que no corrompe comandos

El indice usa embeddings estaticos. Su punto flaco conocido: una pregunta
coloquial en castellano —"como saco una shell reversa"— no se parece
vectorialmente a un fragmento tecnico en ingles con `bash -i >& /dev/tcp/...`.
La busqueda hibrida por palabras ayuda, pero solo si coinciden literales.

La solucion es **indexacion multi-representacion**: por cada fragmento se generan
las preguntas que ese fragmento responde, y se EMBEBE LA PREGUNTA apuntando al
fragmento. Asi la consulta del usuario se compara contra otras preguntas, que es
comparar peras con peras.

Clave para no meter la pata: la respuesta que se le muestra al cerebro es el
**fragmento original, literal** (campo `texto`). El modelo local solo escribe la
PREGUNTA (campo `indexar`). Si inventa una pregunta rara, lo peor que pasa es que
esa entrada no recupere bien; **nunca corrompe un comando**, porque no reescribe
la respuesta. Y el fragmento crudo sigue en el indice por su lado (lo pone
cosecha), asi que la cobertura solo puede subir.

## El modelo

Habla con el ollama local. Usa el modelo que ya este CARGADO (mira /api/ps) para
no forzar a la tarjeta de 4 GB a cambiar de modelo con el carril verde abierto;
si no hay ninguno, carga qwen3.5:4b-jarvis. think:false —qwen3.5 razona por
defecto y tardaria 20x—.

## Reanudable

Un trabajo de horas no puede perder todo si se corta. Se apunta en un .hecho por
que linea de cosecha.jsonl iba, y al arrancar salta hasta ahi. La metodologia
(el oro) se procesa PRIMERO, para que si no termina la noche, lo valioso este.
"""
import argparse
import json
import os
import re
import sys
import urllib.request

AQUI = os.path.dirname(os.path.abspath(__file__))
RAIZ = os.path.dirname(os.path.dirname(AQUI))
COSECHA = os.path.join(RAIZ, "datos", "cosecha.jsonl")
SALIDA = os.path.join(RAIZ, "datos", "sintesis.jsonl")
MARCA = SALIDA + ".hecho"
ENDPOINT = os.environ.get("JARVIS_OLLAMA", "http://127.0.0.1:11434")

INSTRUCCION = """Eres un generador de preguntas para un buscador de apuntes de
pentesting. Te doy un fragmento de apuntes tecnicos. Devuelve de UNA a TRES
preguntas, en castellano, que una persona haria por voz y que ese fragmento
responde.

Reglas:
- Preguntas naturales y variadas, como las diria alguien: "como...", "que
  comando...", "cual es la forma de...".
- NO respondas, NO copies comandos: solo las preguntas.
- Si el fragmento es ruido (un titulo suelto, una lista de enlaces), devuelve
  una lista vacia.

Responde SOLO con JSON: {"preguntas": ["...", "..."]}"""


FIJO = os.environ.get("JARVIS_MODELO")        # si se fija a mano, manda
RESPALDO = "qwen3.5:4b-jarvis"


def _modelo_cargado():
    """El modelo que ollama ya tiene en memoria, para no forzar un cambio."""
    try:
        with urllib.request.urlopen(ENDPOINT + "/api/ps", timeout=5) as r:
            m = json.load(r).get("models", [])
        if m:
            return m[0]["name"]
    except Exception:
        pass
    return None


def _modelo():
    """Que modelo usar AHORA. Se re-mira cada tanto en vez de fijarlo al
    arrancar: en una tarjeta de 4 GB solo cabe uno, asi que si el carril verde
    tiene el suyo cargado, generamos con ESE (una pregunta no necesita el ajuste
    anti-alucinacion, solo la respuesta lo necesitaria y la respuesta es el
    fragmento literal). Asi no se fuerza a ollama a cambiar de modelo en cada
    peticion, que en esta maquina cuesta 2-3 s de recarga.
    """
    return FIJO or _modelo_cargado() or RESPALDO


MODELO = _modelo()


def _pide(fragmento, modelo):
    cuerpo = json.dumps({
        "model": modelo,
        "messages": [{"role": "system", "content": INSTRUCCION},
                     {"role": "user", "content": fragmento[:1400]}],
        "stream": False, "think": False,
        "options": {"temperature": 0.7, "num_predict": 200},
        "format": "json",
    }).encode()
    req = urllib.request.Request(ENDPOINT + "/api/chat", data=cuerpo,
                                 headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=120) as r:
            contenido = json.loads(r.read())["message"]["content"]
    except Exception:
        return []
    try:
        d = json.loads(contenido)
    except json.JSONDecodeError:
        m = re.search(r"\[.*\]", contenido, re.S)
        if not m:
            return []
        try:
            d = {"preguntas": json.loads(m.group(0))}
        except json.JSONDecodeError:
            return []
    preguntas = d.get("preguntas") if isinstance(d, dict) else d
    if not isinstance(preguntas, list):
        return []
    fuera = []
    for p in preguntas:
        if isinstance(p, str) and 8 <= len(p.strip()) <= 200:
            fuera.append(p.strip())
    return fuera[:3]


def fragmentos():
    """Los fragmentos de cosecha, el ORO (metodologia) primero."""
    todos = []
    with open(COSECHA, encoding="utf-8") as f:
        for l in f:
            try:
                todos.append(json.loads(l))
            except json.JSONDecodeError:
                continue
    todos.sort(key=lambda d: 0 if d.get("tipo") == "metodologia" else 1)
    return todos


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--muestra", type=int, default=0)
    a = p.parse_args()

    if not os.path.exists(COSECHA):
        print("  no hay cosecha.jsonl: corre cosecha.py primero")
        return 1

    frags = fragmentos()
    print(f"  {len(frags)} fragmentos  ·  modelo {MODELO}", flush=True)

    if a.muestra:
        for fr in frags[:a.muestra]:
            print(f"\n  ── {fr.get('fichero')} ──")
            for q in _pide(fr["texto"], MODELO):
                print(f"    P: {q}")
        return 0

    desde = 0
    if os.path.exists(MARCA) and os.path.exists(SALIDA):
        try:
            desde = int(open(MARCA).read().strip()) + 1
        except (ValueError, OSError):
            desde = 0
    modo = "a" if desde else "w"
    hechas = sum(1 for _ in open(SALIDA)) if desde and os.path.exists(SALIDA) else 0
    if desde:
        print(f"  reanudando desde el fragmento {desde} ({hechas} preguntas)", flush=True)

    total = len(frags)
    modelo = MODELO
    with open(SALIDA, modo, encoding="utf-8", buffering=1) as out:
        for i in range(desde, total):
            # re-mirar que modelo esta cargado cada 25: si el verde se abre o se
            # cierra durante la noche, seguimos al que este en la tarjeta.
            if i % 25 == 0:
                modelo = _modelo()
            fr = frags[i]
            for q in _pide(fr["texto"], modelo):
                out.write(json.dumps({
                    "tipo": "pregunta",
                    "herramienta": fr.get("herramienta"),
                    "perfil": fr.get("perfil"),
                    "fichero": fr.get("fichero"),
                    "tema": fr.get("tema", ""),
                    "texto": fr["texto"],     # la respuesta: el fragmento LITERAL
                    "indexar": q,             # lo que se embebe: la pregunta
                }, ensure_ascii=False) + "\n")
                hechas += 1
            with open(MARCA, "w") as m:
                m.write(str(i))
            if (i + 1) % 25 == 0:
                print(f"  PROGRESO sintetiza hecho={i+1} total={total} "
                      f"preguntas={hechas}", flush=True)
    print(f"\n  hecho: {hechas} preguntas en {SALIDA}", flush=True)
    return 0


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