#!/usr/bin/env python3
"""
generar_portfolio.py -- Portfolio verificable de proyectos SiPi.

Problema real que ataca: alguien con experiencia real usando SiPi
("llevo un año construyendo cosas con esto") no tiene, hoy, una forma
de PROBARLO ante un reclutador mas alla de "confia en mi" o un link a
un repo que nadie va a leer entero. Este script recorre una carpeta de
proyectos .sipi y genera:

  1. Un reporte HTML de una sola pagina, pensado para mandarle un link
     a un reclutador o pegarlo en un CV/LinkedIn, con:
       - que habilidades (reales, verificables) demuestra cada proyecto
       - si el proyecto CORRE de verdad (no solo "compila")
       - si el analizador de seguridad de SiPi encontro problemas
       - tamaño y complejidad real del código
  2. Un portfolio.json con los mismos datos en formato maquina, para
     que quien reciba el link pueda re-verificarlo el mismo corriendo
     este script sobre la misma carpeta (no es un sello de aprobacion
     que hay que creerle a ciegas al candidato).

Uso:
    python3 generar_portfolio.py <carpeta_con_proyectos> [--nombre "Tu Nombre"]

Cada archivo .sipi encontrado (recursivo) se cuenta como un proyecto.
Si varios archivos comparten carpeta, se agrupan como un solo proyecto
(carpeta = proyecto) para no fragmentar apps de varios archivos.

Este script NO inventa ni exagera nada: si un programa tira un error
real al ejecutarlo, el reporte lo muestra en rojo, tal cual. La idea es
que el reporte sea *mas* creible que un CV justamente porque no puede
mentir sobre estos puntos sin que se note corriendo el script de nuevo.
"""
import sys
import os
import re
import json
import subprocess
import argparse
import datetime

RUTA_ESTE_ARCHIVO = os.path.dirname(os.path.abspath(__file__))
RUTA_SRC = os.path.join(os.path.dirname(RUTA_ESTE_ARCHIVO), "src")
RUTA_SIPI_PY = os.path.join(RUTA_SRC, "sipi.py")

sys.path.insert(0, RUTA_SRC)

try:
    from sipi import _analizar_codigo_estatico  # reusa el linter real del motor
except Exception as e:
    _analizar_codigo_estatico = None
    print(f"[aviso] No se pudo importar el analizador estatico de sipi.py ({e}). "
          f"El reporte va a seguir generandose, pero sin la seccion de seguridad/estilo.")

# Categorias de habilidad -> comandos que la delatan. Pensadas en
# palabras clave que un reclutador tecnico reconoce ("bases de datos",
# "APIs REST", "seguridad", etc.), no en la jerga interna de SiPi.
CATEGORIAS_HABILIDAD = {
    "Bases de datos (SQL real)": [
        "sqlite_", "postgres_", "mysql_", "migracion_",
    ],
    "APIs / backend web": [
        "escuchar_ruta", "iniciar_api_web", "responder_json", "peticion_http",
    ],
    "Paginas web / frontend declarativo": [
        "pagina_web", "generar_pagina_web", "formulario", "tarjeta",
    ],
    "Seguridad aplicada": [
        "hash_seguro_contrasena", "verificar_contrasena", "firmar_hmac",
        "verificar_hmac", "requerir_autenticacion", "limitar_peticiones",
        "generar_contrasena_segura", "generar_token_seguro", "parametrizado",
    ],
    "Programacion orientada a objetos": [
        "clase ", "hereda_de", "interfaz ", "implementa", "nuevo ",
    ],
    "Concurrencia / paralelismo": [
        "hilo_crear", "hilo_resultado", "bloqueo_crear", "con_bloqueo",
    ],
    "Testing / calidad": [
        "afirmar ", "iniciar_pruebas", "resumen_pruebas",
    ],
    "GUI de escritorio": [
        "ventana \"", "entrada ", "menu_desplegable", "pestanias", "casilla ", "barra_progreso",
    ],
    "Videojuegos / graficos": [
        "crear_juego", "sprite ", "colision", "escena_3d", "particulas",
    ],
    "Automatizacion / archivos": [
        "leer_archivo", "crear_archivo", "csv_leer", "csv_guardar",
        "json_leer", "comprimir_carpeta",
    ],
    "IA / datos": [
        "vector_", "regresion_lineal", "estadistica_",
    ],
    "Despliegue / DevOps": [
        "generar_dockerfile", "publicar_nube", "compilar_a_python", "compilar_a_js",
    ],
}

# Comandos que abren algo que NUNCA termina solo (GUI real, servidor
# HTTP real, loop de juego) -- si un proyecto los usa, un timeout al
# ejecutarlo es el comportamiento ESPERADO, no una falla.
COMANDOS_BLOQUEANTES = (
    "ventana ", "crear_juego", "iniciar_api_web", "iniciar_servidor_web",
    "escena_3d", "editor_visual",
)

TIMEOUT_EJECUCION_SEGUNDOS = 6


def contar_lineas_reales(contenido):
    """Lineas no vacias y que no son puramente un comentario."""
    total = 0
    for linea in contenido.split("\n"):
        s = linea.strip()
        if not s or s.startswith("//"):
            continue
        total += 1
    return total


def detectar_habilidades(contenido):
    encontradas = []
    for categoria, comandos in CATEGORIAS_HABILIDAD.items():
        if any(cmd in contenido for cmd in comandos):
            encontradas.append(categoria)
    return encontradas


def es_potencialmente_bloqueante(contenido):
    return any(cmd in contenido for cmd in COMANDOS_BLOQUEANTES)


def ejecutar_y_verificar(ruta_archivo, contenido):
    """Corre el programa real contra el motor de verdad (no un mock) y
    devuelve (estado, detalle). estado es uno de:
    'ok', 'error', 'bloqueante_ok', 'timeout_sospechoso'."""
    try:
        resultado = subprocess.run(
            [sys.executable, RUTA_SIPI_PY, os.path.abspath(ruta_archivo)],
            capture_output=True, text=True, timeout=TIMEOUT_EJECUCION_SEGUNDOS,
            cwd=os.path.dirname(os.path.abspath(ruta_archivo)) or ".",
        )
        if resultado.returncode == 0:
            return "ok", ""
        else:
            detalle = (resultado.stderr or resultado.stdout or "").strip()[-800:]
            return "error", detalle
    except subprocess.TimeoutExpired:
        if es_potencialmente_bloqueante(contenido):
            return "bloqueante_ok", (
                "Arranco sin errores durante los primeros "
                f"{TIMEOUT_EJECUCION_SEGUNDOS}s (contiene GUI/servidor/juego real, "
                "que se queda corriendo esperando eventos -- comportamiento esperado, "
                "no una falla)."
            )
        return "timeout_sospechoso", (
            f"Tardo mas de {TIMEOUT_EJECUCION_SEGUNDOS}s sin usar ningun comando que "
            "deberia bloquear (ventana/servidor/juego). Podria ser un bucle sin "
            "salida -- revisar a mano."
        )
    except Exception as e:
        return "error", str(e)


def analizar_seguridad(contenido):
    if _analizar_codigo_estatico is None:
        return None
    try:
        return _analizar_codigo_estatico(contenido)
    except Exception as e:
        return {"seguridad": [], "bugs": [], "estilo": [], "sugerencias": [],
                "_error_analisis": str(e)}


def descripcion_de(contenido, nombre_archivo):
    m = re.search(r'^\s*programa\s+"([^"]*)"', contenido, re.MULTILINE)
    return m.group(1) if m else nombre_archivo


def recolectar_proyectos(carpeta_raiz):
    """Agrupa archivos .sipi por carpeta contenedora -> un 'proyecto'
    por carpeta (asi apps multi-archivo no se fragmentan)."""
    proyectos = {}
    for dirpath, dirnames, filenames in os.walk(carpeta_raiz):
        dirnames[:] = [d for d in dirnames if not d.endswith("_web") and d != "__pycache__"]
        archivos_sipi = sorted(f for f in filenames if f.endswith(".sipi"))
        if not archivos_sipi:
            continue
        clave = os.path.relpath(dirpath, carpeta_raiz)
        proyectos.setdefault(clave, []).append((dirpath, archivos_sipi))
    return proyectos


def procesar_proyecto(nombre_proyecto, archivos_info, verbose=True):
    archivos_resultado = []
    habilidades_proyecto = set()
    lineas_totales = 0
    hallazgos_seguridad_totales = 0
    estados = []

    for dirpath, archivos in archivos_info:
        for nombre_archivo in archivos:
            ruta = os.path.join(dirpath, nombre_archivo)
            with open(ruta, "r", encoding="utf-8", errors="replace") as f:
                contenido = f.read()

            lineas = contar_lineas_reales(contenido)
            lineas_totales += lineas
            habilidades_proyecto.update(detectar_habilidades(contenido))

            if verbose:
                print(f"  - Ejecutando {nombre_archivo} ...", flush=True)
            estado, detalle = ejecutar_y_verificar(ruta, contenido)
            estados.append(estado)

            reporte_seg = analizar_seguridad(contenido)
            n_hallazgos_seg = len(reporte_seg["seguridad"]) if reporte_seg else 0
            hallazgos_seguridad_totales += n_hallazgos_seg

            archivos_resultado.append({
                "archivo": nombre_archivo,
                "descripcion": descripcion_de(contenido, nombre_archivo),
                "lineas": lineas,
                "estado_ejecucion": estado,
                "detalle_ejecucion": detalle,
                "hallazgos_seguridad": n_hallazgos_seg,
            })

    if "error" in estados:
        estado_proyecto = "error"
    elif "timeout_sospechoso" in estados:
        estado_proyecto = "timeout_sospechoso"
    else:
        estado_proyecto = "ok"

    return {
        "nombre": nombre_proyecto,
        "archivos": archivos_resultado,
        "habilidades": sorted(habilidades_proyecto),
        "lineas_totales": lineas_totales,
        "estado": estado_proyecto,
        "hallazgos_seguridad": hallazgos_seguridad_totales,
    }


ESTILO_CSS = """
* { box-sizing: border-box; }
body { font-family: "Segoe UI", system-ui, sans-serif; background: #0f0f17; color: #e6e6f0;
       max-width: 900px; margin: 0 auto; padding: 40px 20px; line-height: 1.5; }
h1 { color: #7ec4ff; font-size: 2rem; margin-bottom: 4px; }
.subtitulo { color: #8a8fa3; margin-bottom: 30px; }
.resumen { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 40px; }
.metrica { background: #1a1a2a; border: 1px solid #2a2a3d; border-radius: 12px;
           padding: 16px 20px; flex: 1; min-width: 140px; }
.metrica .numero { font-size: 1.8rem; font-weight: bold; color: #7ec4ff; }
.metrica .etiqueta { color: #8a8fa3; font-size: 0.85rem; }
.proyecto { background: #1a1a2a; border: 1px solid #2a2a3d; border-radius: 12px;
            padding: 24px; margin-bottom: 20px; }
.proyecto h2 { margin: 0 0 8px 0; font-size: 1.3rem; }
.badge { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 0.78rem;
         margin: 2px 4px 2px 0; }
.badge-ok { background: #1e3a2a; color: #6fd68f; }
.badge-error { background: #3a1e1e; color: #f28b8b; }
.badge-warn { background: #3a2f1e; color: #f2c98b; }
.badge-skill { background: #1e2a3a; color: #7ec4ff; }
.archivo-detalle { font-size: 0.85rem; color: #b0b3c0; margin-top: 10px; }
.detalle-error { background: #12060a; color: #f28b8b; padding: 10px; border-radius: 8px;
                 font-family: monospace; font-size: 0.78rem; white-space: pre-wrap;
                 margin-top: 6px; max-height: 200px; overflow: auto; }
footer { color: #6c7086; text-align: center; margin-top: 40px; font-size: 0.85rem; }
footer a { color: #7ec4ff; }
"""


def generar_html(nombre_dueno, proyectos, carpeta_raiz):
    total_proyectos = len(proyectos)
    total_lineas = sum(p["lineas_totales"] for p in proyectos)
    total_ok = sum(1 for p in proyectos if p["estado"] == "ok")
    total_hallazgos_seg = sum(p["hallazgos_seguridad"] for p in proyectos)
    todas_habilidades = sorted({h for p in proyectos for h in p["habilidades"]})

    metricas_html = f"""
    <div class="metrica"><div class="numero">{total_proyectos}</div><div class="etiqueta">Proyectos verificados</div></div>
    <div class="metrica"><div class="numero">{total_lineas}</div><div class="etiqueta">Lineas de codigo real</div></div>
    <div class="metrica"><div class="numero">{len(todas_habilidades)}</div><div class="etiqueta">Areas de habilidad cubiertas</div></div>
    <div class="metrica"><div class="numero">{total_ok}/{total_proyectos}</div><div class="etiqueta">Ejecutan sin errores</div></div>
    """

    proyectos_html = []
    for p in proyectos:
        badges_estado = {
            "ok": '<span class="badge badge-ok">✓ Ejecuta sin errores</span>',
            "bloqueante_ok": '<span class="badge badge-ok">✓ Arranca sin errores (app con GUI/servidor)</span>',
            "error": '<span class="badge badge-error">✗ Error al ejecutar</span>',
            "timeout_sospechoso": '<span class="badge badge-warn">⚠ Revisar (tardo demasiado)</span>',
        }
        skills_html = "".join(f'<span class="badge badge-skill">{h}</span>' for h in p["habilidades"])
        seg_html = (
            '<span class="badge badge-ok">✓ 0 hallazgos de seguridad</span>'
            if p["hallazgos_seguridad"] == 0
            else f'<span class="badge badge-warn">⚠ {p["hallazgos_seguridad"]} hallazgo(s) de seguridad a revisar</span>'
        )

        archivos_html = []
        for a in p["archivos"]:
            detalle_error_html = ""
            if a["estado_ejecucion"] in ("error", "timeout_sospechoso") and a["detalle_ejecucion"]:
                detalle_error_html = f'<div class="detalle-error">{a["detalle_ejecucion"]}</div>'
            archivos_html.append(
                f'<div class="archivo-detalle">📄 <b>{a["archivo"]}</b> -- "{a["descripcion"]}" '
                f'({a["lineas"]} lineas){detalle_error_html}</div>'
            )

        proyectos_html.append(f"""
    <div class="proyecto">
      <h2>{p['nombre']}</h2>
      <div>{badges_estado.get(p['estado'], '')} {seg_html}</div>
      <div style="margin-top:10px;">{skills_html}</div>
      {''.join(archivos_html)}
    </div>
        """)

    ahora = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
    return f"""<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Portfolio SiPi -- {nombre_dueno}</title>
<style>{ESTILO_CSS}</style>
</head>
<body>
  <h1>Portfolio de {nombre_dueno}</h1>
  <p class="subtitulo">Proyectos construidos con <a href="https://github.com/" style="color:#7ec4ff;">SiPi</a>,
     verificados automaticamente (no autodeclarados) el {ahora}.</p>

  <div class="resumen">{metricas_html}</div>

  <h2 style="color:#e6e6f0;">Proyectos</h2>
  {''.join(proyectos_html)}

  <footer>
    Generado con <code>herramientas/generar_portfolio.py</code>.
    Cualquiera puede re-verificar este reporte corriendo el mismo script
    sobre esta misma carpeta de proyectos -- no es una autodeclaracion.
  </footer>
</body>
</html>
"""


def main():
    parser = argparse.ArgumentParser(description="Genera un portfolio verificable de proyectos SiPi.")
    parser.add_argument("carpeta", help="Carpeta que contiene los proyectos .sipi (busqueda recursiva)")
    parser.add_argument("--nombre", default="Candidato/a", help="Nombre a mostrar en el portfolio")
    parser.add_argument("--salida", default="portfolio", help="Prefijo de los archivos de salida (sin extension)")
    args = parser.parse_args()

    if not os.path.isdir(args.carpeta):
        print(f"[error] La carpeta '{args.carpeta}' no existe.")
        sys.exit(1)

    print(f"[SiPi Portfolio] Buscando proyectos en '{args.carpeta}' ...")
    agrupados = recolectar_proyectos(args.carpeta)
    if not agrupados:
        print("[error] No se encontro ningun archivo .sipi en esa carpeta.")
        sys.exit(1)

    proyectos = []
    for nombre_proyecto, archivos_info in sorted(agrupados.items()):
        print(f"[SiPi Portfolio] Proyecto: {nombre_proyecto}")
        proyectos.append(procesar_proyecto(nombre_proyecto, archivos_info))

    html = generar_html(args.nombre, proyectos, args.carpeta)
    ruta_html = f"{args.salida}.html"
    ruta_json = f"{args.salida}.json"
    with open(ruta_html, "w", encoding="utf-8") as f:
        f.write(html)
    with open(ruta_json, "w", encoding="utf-8") as f:
        json.dump({
            "generado": datetime.datetime.now().isoformat(),
            "nombre": args.nombre,
            "proyectos": proyectos,
        }, f, ensure_ascii=False, indent=2)

    total_ok = sum(1 for p in proyectos if p["estado"] == "ok" or p["estado"] == "bloqueante_ok")
    print(f"\n[SiPi Portfolio] Listo: {len(proyectos)} proyecto(s), {total_ok} sin errores.")
    print(f"[SiPi Portfolio] HTML: {ruta_html}")
    print(f"[SiPi Portfolio] JSON: {ruta_json}")


if __name__ == "__main__":
    main()
