#!/usr/bin/env python3
"""
generar_docs.py - Genera documentacion HTML a partir de comentarios
especiales '//!' escritos dentro de funciones/clases/metodos/interfaces.

`sipi_cli.py` (`cmd_doc`) ya buscaba este archivo exacto desde la
v41.24 (`sipi doc archivo.sipi` -> `subprocess.run([sys.executable,
os.path.join(AQUI, "generar_docs.py"), archivo])`), y el propio
CHANGELOG describia el formato con un ejemplo concreto, pero el
archivo nunca habia llegado a este paquete: `sipi doc` fallaba
siempre con un error de "No such file or directory" de Python al
intentar correr un script que no existia.

Formato esperado (tal como lo documenta el CHANGELOG, v41.24):

    funcion sumar(a, b)
        //! Suma dos numeros y devuelve el resultado.
        //! @param a: primer numero
        //! @param b: segundo numero
        //! @returns: a + b
        devolver a + b
    fin

Los '//!' van como primeras lineas DENTRO del bloque (no arriba de la
declaracion) -- se leen desde ahi hasta la primera linea que no
empiece con '//!'. '@param nombre: descripcion' y '@returns:
descripcion' se muestran en su propia fila; el resto de las lineas
'//!' se juntan como descripcion general.

Funciona sobre el archivo fuente en texto plano -- no necesita que el
programa corra sin errores (a diferencia de un generador que
importara/ejecutara el modulo para inspeccionarlo).

Uso:
    python3 generar_docs.py archivo.sipi
    (via el CLI: sipi doc archivo.sipi)

Genera 'archivo_docs.html' al lado del archivo de entrada.
"""
import html
import os
import re
import sys

PATRON_FUNCION = re.compile(r"^(\s*)funcion\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?\s*$")
PATRON_METODO = re.compile(r"^(\s*)metodo\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\w+))?\s*$")
PATRON_CLASE = re.compile(r"^(\s*)clase\s+(\w+)(?:\s+hereda_de\s+(\w+))?(?:\s+implementa\s+(\w+))?\s*$")
PATRON_INTERFAZ = re.compile(r"^(\s*)interfaz\s+(\w+)\s*$")
PATRON_DOC = re.compile(r"^\s*//!\s?(.*)$")
PATRON_PARAM = re.compile(r"^@param\s+(\w+)\s*:\s*(.*)$")
PATRON_RETURNS = re.compile(r"^@returns?\s*:\s*(.*)$")


def _leer_bloque_doc(lineas, indice_declaracion):
    """Lee las lineas '//!' que arrancan inmediatamente despues de la
    linea de declaracion (indice_declaracion), y devuelve
    (descripcion_general, {param: desc}, retorno_desc) -- o None si no
    hay ningun '//!' ahi."""
    descripcion = []
    parametros = {}
    retorno = None
    encontro_algo = False
    i = indice_declaracion + 1
    while i < len(lineas):
        m = PATRON_DOC.match(lineas[i])
        if not m:
            break
        encontro_algo = True
        texto = m.group(1).strip()
        m_param = PATRON_PARAM.match(texto)
        m_ret = PATRON_RETURNS.match(texto)
        if m_param:
            parametros[m_param.group(1)] = m_param.group(2).strip()
        elif m_ret:
            retorno = m_ret.group(1).strip()
        elif texto:
            descripcion.append(texto)
        i += 1
    if not encontro_algo:
        return None
    return " ".join(descripcion), parametros, retorno


def _indentacion(linea):
    return len(linea) - len(linea.lstrip(" "))


def extraer_elementos(texto_fuente):
    """Recorre el archivo fuente linea por linea (sin ejecutarlo) y
    devuelve una lista de elementos documentables: funciones sueltas,
    interfaces, y clases con sus metodos anidados (se determina que
    metodo pertenece a que clase por indentacion: el metodo pertenece
    a la clase abierta mas cercana hacia arriba con MENOR indentacion,
    que es la convencion que ya usa todo el codigo de este proyecto)."""
    lineas = texto_fuente.splitlines()
    elementos = []
    pila_clases = []  # [(indentacion, elemento_clase)]

    for i, linea in enumerate(lineas):
        m_clase = PATRON_CLASE.match(linea)
        m_interfaz = PATRON_INTERFAZ.match(linea)
        m_funcion = PATRON_FUNCION.match(linea)
        m_metodo = PATRON_METODO.match(linea)

        if m_clase:
            indent = len(m_clase.group(1))
            while pila_clases and pila_clases[-1][0] >= indent:
                pila_clases.pop()
            doc = _leer_bloque_doc(lineas, i)
            elemento = {
                "tipo": "clase",
                "nombre": m_clase.group(2),
                "hereda_de": m_clase.group(3),
                "implementa": m_clase.group(4),
                "doc": doc,
                "metodos": [],
                "linea": i + 1,
            }
            elementos.append(elemento)
            pila_clases.append((indent, elemento))

        elif m_interfaz:
            doc = _leer_bloque_doc(lineas, i)
            elementos.append({
                "tipo": "interfaz",
                "nombre": m_interfaz.group(2),
                "doc": doc,
                "linea": i + 1,
            })

        elif m_metodo:
            indent = len(m_metodo.group(1))
            while pila_clases and pila_clases[-1][0] >= indent:
                pila_clases.pop()
            doc = _leer_bloque_doc(lineas, i)
            metodo = {
                "tipo": "metodo",
                "nombre": m_metodo.group(2),
                "parametros": [p.strip() for p in m_metodo.group(3).split(",") if p.strip()],
                "retorno_tipo": m_metodo.group(4),
                "doc": doc,
                "linea": i + 1,
            }
            if pila_clases:
                pila_clases[-1][1]["metodos"].append(metodo)
            else:
                # metodo() suelto fuera de cualquier clase -- no deberia
                # pasar en un programa valido, pero se documenta igual
                # en vez de perderlo silenciosamente.
                elementos.append(metodo)

        elif m_funcion:
            doc = _leer_bloque_doc(lineas, i)
            elementos.append({
                "tipo": "funcion",
                "nombre": m_funcion.group(2),
                "parametros": [p.strip() for p in m_funcion.group(3).split(",") if p.strip()],
                "retorno_tipo": m_funcion.group(4),
                "doc": doc,
                "linea": i + 1,
            })

    return elementos


def _render_doc(doc, parametros_declarados):
    if doc is None:
        return '<p class="sin-doc">Sin documentacion todavia.</p>'
    descripcion, params_doc, retorno = doc
    partes = []
    if descripcion:
        partes.append(f'<p class="descripcion">{html.escape(descripcion)}</p>')
    filas_param = []
    for p in parametros_declarados:
        nombre_param = p.split(":")[0].strip()
        desc = params_doc.get(nombre_param)
        filas_param.append(
            f'<tr><td class="param-nombre">{html.escape(nombre_param)}</td>'
            f'<td>{html.escape(desc) if desc else "<span class=\'sin-doc\'>sin descripcion</span>"}</td></tr>'
        )
    if filas_param:
        partes.append(
            '<table class="tabla-params"><thead><tr><th>Parametro</th><th>Descripcion</th></tr></thead>'
            f'<tbody>{"".join(filas_param)}</tbody></table>'
        )
    if retorno:
        partes.append(f'<p class="retorno"><strong>Devuelve:</strong> {html.escape(retorno)}</p>')
    return "\n".join(partes) if partes else '<p class="sin-doc">Sin documentacion todavia.</p>'


def _render_funcion_o_metodo(elemento):
    firma_params = ", ".join(elemento["parametros"])
    tipo_retorno = f' -&gt; {html.escape(elemento["retorno_tipo"])}' if elemento.get("retorno_tipo") else ""
    return f"""
    <div class="miembro">
      <h4><code>{html.escape(elemento['nombre'])}({html.escape(firma_params)}){tipo_retorno}</code>
        <span class="linea">linea {elemento['linea']}</span></h4>
      {_render_doc(elemento['doc'], elemento['parametros'])}
    </div>"""


def generar_html(texto_fuente, nombre_archivo):
    elementos = extraer_elementos(texto_fuente)

    bloques = []
    for el in elementos:
        if el["tipo"] == "funcion":
            bloques.append(f"""
  <section class="elemento">
    <h3>&#402; funcion <code>{html.escape(el['nombre'])}</code></h3>
    {_render_funcion_o_metodo(el)}
  </section>""")
        elif el["tipo"] == "interfaz":
            bloques.append(f"""
  <section class="elemento">
    <h3>&#9671; interfaz <code>{html.escape(el['nombre'])}</code></h3>
    {_render_doc(el['doc'], [])}
  </section>""")
        elif el["tipo"] == "clase":
            extra = []
            if el.get("hereda_de"):
                extra.append(f"hereda de <code>{html.escape(el['hereda_de'])}</code>")
            if el.get("implementa"):
                extra.append(f"implementa <code>{html.escape(el['implementa'])}</code>")
            extra_txt = f' <span class="extra">({", ".join(extra)})</span>' if extra else ""
            metodos_html = "".join(_render_funcion_o_metodo(m) for m in el["metodos"]) or '<p class="sin-doc">Esta clase no tiene metodos.</p>'
            bloques.append(f"""
  <section class="elemento">
    <h3>&#9632; clase <code>{html.escape(el['nombre'])}</code>{extra_txt}</h3>
    {_render_doc(el['doc'], [])}
    <div class="metodos">{metodos_html}</div>
  </section>""")
        elif el["tipo"] == "metodo":
            bloques.append(f"""
  <section class="elemento">
    <h3>metodo suelto <code>{html.escape(el['nombre'])}</code></h3>
    {_render_funcion_o_metodo(el)}
  </section>""")

    if not bloques:
        bloques.append('<p class="sin-doc">No se encontro ninguna funcion, clase, metodo ni interfaz en este archivo.</p>')

    total = len(elementos)
    documentados = sum(1 for e in elementos if e.get("doc") is not None) + sum(
        1 for e in elementos if e["tipo"] == "clase" for m in e["metodos"] if m.get("doc") is not None
    )
    total_con_metodos = total + sum(len(e["metodos"]) for e in elementos if e["tipo"] == "clase")

    return f"""<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Documentacion de {html.escape(nombre_archivo)}</title>
<style>
  body {{
    background: #1e1e2e; color: #cdd6f4;
    font-family: "Segoe UI", system-ui, sans-serif;
    max-width: 860px; margin: 0 auto; padding: 32px 24px 64px;
    line-height: 1.5;
  }}
  h1 {{ color: #cba6f7; margin-bottom: 4px; }}
  .subtitulo {{ color: #a6adc8; margin-top: 0; margin-bottom: 32px; }}
  h3 {{ color: #89b4fa; border-bottom: 1px solid #313244; padding-bottom: 8px; }}
  h4 {{ color: #f9e2af; margin-bottom: 6px; }}
  code {{ background: #313244; padding: 1px 6px; border-radius: 4px; font-family: "Cascadia Code", Consolas, monospace; }}
  .elemento {{ margin-bottom: 36px; }}
  .miembro {{ margin: 14px 0 14px 18px; padding-left: 14px; border-left: 2px solid #45475a; }}
  .descripcion {{ margin: 6px 0; }}
  .sin-doc {{ color: #6c7086; font-style: italic; }}
  .linea {{ color: #6c7086; font-size: 0.8em; font-weight: normal; margin-left: 10px; }}
  .extra {{ color: #a6adc8; font-size: 0.85em; font-weight: normal; }}
  .tabla-params {{ border-collapse: collapse; margin: 8px 0; width: 100%; }}
  .tabla-params th, .tabla-params td {{ text-align: left; padding: 4px 10px; border-bottom: 1px solid #313244; }}
  .tabla-params th {{ color: #a6adc8; font-weight: 600; }}
  .param-nombre {{ font-family: monospace; color: #f9e2af; }}
  .retorno {{ margin: 6px 0; }}
  footer {{ margin-top: 48px; color: #6c7086; font-size: 0.85em; }}
</style>
</head>
<body>
  <h1>{html.escape(nombre_archivo)}</h1>
  <p class="subtitulo">{documentados}/{total_con_metodos} elemento(s) documentado(s) con <code>//!</code></p>
  {''.join(bloques)}
  <footer>Generado por <code>generar_docs.py</code> (SiPi) a partir del codigo fuente, sin ejecutar el programa.</footer>
</body>
</html>
"""


def main():
    if len(sys.argv) < 2:
        print("Uso: python3 generar_docs.py archivo.sipi")
        sys.exit(1)

    ruta_entrada = sys.argv[1]
    if not os.path.exists(ruta_entrada):
        print(f"[SiPi] Error: no se encontro el archivo '{ruta_entrada}'")
        sys.exit(1)

    with open(ruta_entrada, "r", encoding="utf-8") as f:
        texto_fuente = f.read()

    nombre_base = os.path.splitext(os.path.basename(ruta_entrada))[0]
    html_generado = generar_html(texto_fuente, os.path.basename(ruta_entrada))

    ruta_salida = os.path.join(os.path.dirname(os.path.abspath(ruta_entrada)), f"{nombre_base}_docs.html")
    with open(ruta_salida, "w", encoding="utf-8") as f:
        f.write(html_generado)

    print(f"[SiPi] Documentacion generada: {ruta_salida}")


if __name__ == "__main__":
    main()
