#!/usr/bin/env python3
"""Cataloga las herramientas de COFRE para que JARVIS sepa cuales hay.

    python3 ~/COFRE/CODERS/JARVIS/config/descubrir.py

Produce jarvis-herramientas.json: una ficha por herramienta con su nombre, que
hace, como se invoca y en que carpeta vive. NO es el catalogo de acciones
rapidas (esas son las ~20 que dices por voz a diario, escritas a mano). Esto es
el registro que el modelo consulta cuando le pides algo que no esta en el
catalogo, para componer el comando en vez de inventarselo.

Que hace y que NO hace:
  - SI: encuentra los puntos de entrada reales (scripts con shebang y binarios
    en los primeros niveles de cada proyecto, no los modulos internos).
  - SI: saca una descripcion del README, del docstring o de la cabecera, SIN
    ejecutar nada. Ejecutar un script desconocido con --help para leerlo seria
    ejecutar un script desconocido, que es justo lo que no se quiere hacer a
    ciegas.
  - NO: ejecuta ninguna herramienta. Esto solo mira.

El resultado se revisa a mano antes de usarlo: es material de terceros y algun
"tool" sera un ejemplo o un exploit, no algo que quieras ofrecer.
"""
import ast
import json
import os
import re
import stat
import sys

COFRE = os.path.expanduser("~/COFRE")
SALIDA = os.path.expanduser("~/COFRE/CODERS/JARVIS/config/jarvis-herramientas.json")

CARPETAS = ["CODERS", "PENTESTERS", "AUTOMATAS", "DOSER", "HARD-WARERS",
            "PHISHERS", "PROTECTORS", "REVERSERS", "SNEAKERS"]

# Solo los primeros niveles: un punto de entrada vive cerca de la raiz del
# proyecto, no enterrado en lib/ o en site-packages.
PROFUNDIDAD = 3

FUERA = {"node_modules", ".git", "venv", ".venv", "site-packages", "__pycache__",
         "lib", "libs", "tests", "test", "docs", "examples", "vendor", "dist",
         "build", ".tox", "exploitdb"}

# Nombres que casi nunca son la herramienta en si
IGNORAR_NOMBRES = {"setup.py", "conftest.py", "__init__.py", "test.py",
                   "tests.py", "wsgi.py", "manage.py", "conf.py"}


def es_ejecutable(ruta: str) -> bool:
    try:
        return bool(os.stat(ruta).st_mode & stat.S_IXUSR)
    except OSError:
        return False


def tiene_shebang(ruta: str) -> bool:
    try:
        with open(ruta, "rb") as f:
            return f.read(2) == b"#!"
    except OSError:
        return False


def descripcion_readme(carpeta: str) -> str:
    """Primera linea con contenido de un README cercano."""
    for nombre in ("README.md", "README.rst", "README.txt", "README"):
        ruta = os.path.join(carpeta, nombre)
        if not os.path.exists(ruta):
            continue
        try:
            with open(ruta, errors="ignore") as f:
                for linea in f:
                    limpia = linea.strip().lstrip("#").lstrip(">").strip()
                    # saltar titulos de una palabra, badges y separadores
                    if len(limpia) > 25 and not limpia.startswith(("![", "[!", "---", "===")):
                        return limpia[:200]
        except OSError:
            pass
    return ""


def descripcion_script(ruta: str) -> str:
    """Docstring del modulo (Python) o cabecera de comentarios (shell)."""
    try:
        with open(ruta, errors="ignore") as f:
            texto = f.read(4000)
    except OSError:
        return ""

    if ruta.endswith(".py"):
        try:
            doc = ast.get_docstring(ast.parse(texto))
            if doc:
                return doc.strip().split("\n")[0][:200]
        except (SyntaxError, ValueError):
            pass

    # cabecera de comentarios: las primeras lineas que empiezan por # tras el shebang
    lineas = []
    for linea in texto.splitlines()[1:8]:
        s = linea.strip()
        if s.startswith("#"):
            limpio = s.lstrip("#").strip()
            if len(limpio) > 15:
                lineas.append(limpio)
        elif lineas:
            break
    return " ".join(lineas)[:200]


def recorre():
    herramientas = []
    vistos = set()

    for carpeta in CARPETAS:
        raiz = os.path.join(COFRE, carpeta)
        if not os.path.isdir(raiz):
            continue

        for dirpath, dirnames, ficheros in os.walk(raiz):
            profundidad = dirpath[len(raiz):].count(os.sep)
            if profundidad >= PROFUNDIDAD:
                dirnames[:] = []
                continue
            dirnames[:] = [d for d in dirnames
                           if d not in FUERA and not d.startswith(".")]

            for nombre in ficheros:
                if nombre in IGNORAR_NOMBRES:
                    continue
                ruta = os.path.join(dirpath, nombre)
                ext = os.path.splitext(nombre)[1].lower()

                es_script = ext in (".py", ".sh") and (tiene_shebang(ruta) or es_ejecutable(ruta))
                es_binario = not ext and es_ejecutable(ruta) and not tiene_shebang(ruta)
                if not (es_script or es_binario):
                    continue

                clave = nombre.lower()
                if clave in vistos:
                    continue
                vistos.add(clave)

                desc = descripcion_script(ruta) if es_script else ""
                if not desc:
                    desc = descripcion_readme(dirpath)

                herramientas.append({
                    "nombre": os.path.splitext(nombre)[0],
                    "carpeta": carpeta,
                    "ruta": ruta,
                    "tipo": "python" if ext == ".py" else "shell" if ext == ".sh" else "binario",
                    "descripcion": desc,
                    "invocar": (f"python3 {ruta}" if ext == ".py"
                                else f"bash {ruta}" if ext == ".sh"
                                else ruta),
                })

    return herramientas


def main():
    print("recorriendo COFRE (solo lectura, no se ejecuta nada)...\n", flush=True)
    tools = recorre()

    porcarpeta = {}
    con_desc = 0
    for t in tools:
        porcarpeta[t["carpeta"]] = porcarpeta.get(t["carpeta"], 0) + 1
        if t["descripcion"]:
            con_desc += 1

    for carpeta in CARPETAS:
        if carpeta in porcarpeta:
            print(f"  {carpeta:<13} {porcarpeta[carpeta]:>4} herramientas")
    print(f"\n  total {len(tools)}, con descripcion {con_desc} "
          f"({100*con_desc//max(1,len(tools))} %)")

    with open(SALIDA, "w") as f:
        json.dump({"version": 1, "herramientas": tools}, f,
                  indent=2, ensure_ascii=False)
    print(f"\nescrito {SALIDA}")
    print("REVISALO a mano antes de usarlo: es material de terceros y algun")
    print("'tool' sera un ejemplo o un exploit, no algo que quieras ofrecer.")
    return 0


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