#!/usr/bin/env python3
"""
Generador del Documento .docx — Semana 3 Procesamiento Natural del Lenguaje (NRC-8774)
======================================================================================
Genera: Oviedo_Alexander_Proyecto_S3_PNL.docx
Formato: APA 7ª edición (Calibri 11pt, interlineado doble, márgenes 2.54 cm)
"""

import os
import sys

# Verificar dependencia
try:
    from docx import Document
    from docx.shared import Inches, Pt, Cm, RGBColor
    from docx.enum.text import WD_ALIGN_PARAGRAPH
    from docx.enum.table import WD_TABLE_ALIGNMENT
    from docx.oxml.ns import qn
    from docx.oxml import OxmlElement
except ImportError:
    print("Instalando python-docx...")
    os.system(f"{sys.executable} -m pip install python-docx")
    from docx import Document
    from docx.shared import Inches, Pt, Cm, RGBColor
    from docx.enum.text import WD_ALIGN_PARAGRAPH
    from docx.enum.table import WD_TABLE_ALIGNMENT
    from docx.oxml.ns import qn
    from docx.oxml import OxmlElement


# ─── Constantes APA 7 ─────────────────────────────────────────────────
FONT_NAME = "Calibri"
FONT_SIZE = Pt(11)
LINE_SPACING = 2.0
MARGIN = Cm(2.54)
INDENT = Cm(1.27)

COLOR_TITULO = RGBColor(0x0B, 0x13, 0x2B)
COLOR_SUBTITULO = RGBColor(0x1A, 0x5F, 0x7A)
COLOR_TEXTO = RGBColor(0x33, 0x33, 0x33)


# ─── Funciones auxiliares ──────────────────────────────────────────────

def set_cell_shading(cell, color_hex):
    shading = OxmlElement('w:shd')
    shading.set(qn('w:fill'), color_hex)
    shading.set(qn('w:val'), 'clear')
    cell._tc.get_or_add_tcPr().append(shading)


def add_paragraph_apa(doc, text, bold=False, italic=False,
                      alignment=WD_ALIGN_PARAGRAPH.LEFT,
                      indent=True, font_size=FONT_SIZE,
                      space_after=Pt(0)):
    p = doc.add_paragraph()
    run = p.add_run(text)
    run.font.name = FONT_NAME
    run.font.size = font_size
    run.font.bold = bold
    run.font.italic = italic
    run.font.color.rgb = COLOR_TEXTO

    pf = p.paragraph_format
    pf.alignment = alignment
    pf.line_spacing = LINE_SPACING
    pf.space_after = space_after
    pf.space_before = Pt(0)
    if indent:
        pf.first_line_indent = INDENT
    return p


def add_heading_apa(doc, text, level=1):
    p = doc.add_paragraph()
    run = p.add_run(text)
    run.font.name = FONT_NAME
    run.font.color.rgb = COLOR_TITULO

    pf = p.paragraph_format
    pf.line_spacing = LINE_SPACING
    pf.space_before = Pt(12)
    pf.space_after = Pt(6)

    if level == 1:
        run.font.size = Pt(14)
        run.font.bold = True
        pf.alignment = WD_ALIGN_PARAGRAPH.CENTER
    elif level == 2:
        run.font.size = Pt(12)
        run.font.bold = True
        pf.alignment = WD_ALIGN_PARAGRAPH.LEFT
    elif level == 3:
        run.font.size = Pt(11)
        run.font.bold = True
        run.font.italic = True
        pf.alignment = WD_ALIGN_PARAGRAPH.LEFT
    return p


def add_table(doc, headers, rows, col_widths=None):
    table = doc.add_table(rows=1 + len(rows), cols=len(headers))
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.style = 'Table Grid'

    for i, header in enumerate(headers):
        cell = table.rows[0].cells[i]
        cell.text = header
        set_cell_shading(cell, '0B132B')
        for paragraph in cell.paragraphs:
            paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
            for run in paragraph.runs:
                run.font.name = FONT_NAME
                run.font.size = Pt(10)
                run.font.bold = True
                run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)

    for r, row_data in enumerate(rows):
        for c, cell_text in enumerate(row_data):
            cell = table.rows[r + 1].cells[c]
            cell.text = str(cell_text)
            for paragraph in cell.paragraphs:
                paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
                for run in paragraph.runs:
                    run.font.name = FONT_NAME
                    run.font.size = Pt(10)
                    run.font.color.rgb = COLOR_TEXTO

    if col_widths:
        for i, width in enumerate(col_widths):
            for row in table.rows:
                row.cells[i].width = width

    doc.add_paragraph()
    return table


def add_reference(doc, text):
    p = doc.add_paragraph()
    run = p.add_run(text)
    run.font.name = FONT_NAME
    run.font.size = Pt(11)
    run.font.color.rgb = COLOR_TEXTO

    pf = p.paragraph_format
    pf.alignment = WD_ALIGN_PARAGRAPH.LEFT
    pf.line_spacing = LINE_SPACING
    pf.space_after = Pt(0)
    pf.left_indent = INDENT
    pf.first_line_indent = -INDENT
    return p


def add_bullet(doc, text, bold_prefix="", level=0):
    """Agrega un párrafo tipo bullet (viñeta)."""
    p = doc.add_paragraph()
    if bold_prefix:
        run_bold = p.add_run(bold_prefix)
        run_bold.font.name = FONT_NAME
        run_bold.font.size = FONT_SIZE
        run_bold.font.bold = True
        run_bold.font.color.rgb = COLOR_TEXTO

    run = p.add_run(text)
    run.font.name = FONT_NAME
    run.font.size = FONT_SIZE
    run.font.color.rgb = COLOR_TEXTO

    pf = p.paragraph_format
    pf.line_spacing = LINE_SPACING
    pf.space_after = Pt(2)
    pf.left_indent = Cm(1.27 + level * 0.63)
    pf.first_line_indent = Cm(-0.63)

    # Añadir símbolo de viñeta
    bullet_char = "•" if level == 0 else "◦"
    first_run = p.runs[0] if p.runs else p.add_run("")
    first_run.text = f"{bullet_char} " + first_run.text

    return p


# ─── Construcción del Documento ───────────────────────────────────────

def build_document():
    doc = Document()

    # Configurar márgenes
    for section in doc.sections:
        section.top_margin = MARGIN
        section.bottom_margin = MARGIN
        section.left_margin = MARGIN
        section.right_margin = MARGIN

    # ═══════════════════════════════════════════════════════════════
    # PORTADA
    # ═══════════════════════════════════════════════════════════════
    for _ in range(6):
        p = doc.add_paragraph()
        p.paragraph_format.line_spacing = LINE_SPACING

    add_paragraph_apa(doc,
        "Proyecto: Procesamiento Natural del Lenguaje — "
        "Tokenización, Limpieza, NER y Análisis de Frecuencias",
        bold=True, alignment=WD_ALIGN_PARAGRAPH.CENTER,
        indent=False, font_size=Pt(14))

    doc.add_paragraph().paragraph_format.line_spacing = LINE_SPACING

    autores = [
        "Alexander Oviedo Fadul",
        "Maria Fernanda Ruiz Paipilla",
        "Neheman Samir Jaller Cerchiaro",
        "William David Obando Lopez",
    ]
    for autor in autores:
        add_paragraph_apa(doc, autor,
            alignment=WD_ALIGN_PARAGRAPH.CENTER, indent=False)

    doc.add_paragraph().paragraph_format.line_spacing = LINE_SPACING

    datos = [
        "Grupo 7",
        "Especialización en Inteligencia Artificial",
        "Corporación Universitaria Minuto de Dios (UNIMINUTO)",
        "NRC-8774 — Procesamiento Natural del Lenguaje — Semana 3",
        "Docente: Nathalia Orozco Morales",
        "Julio de 2026",
    ]
    for dato in datos:
        add_paragraph_apa(doc, dato,
            alignment=WD_ALIGN_PARAGRAPH.CENTER, indent=False)

    doc.add_page_break()

    # ═══════════════════════════════════════════════════════════════
    # ÍNDICE
    # ═══════════════════════════════════════════════════════════════
    add_heading_apa(doc, "Tabla de Contenidos", level=1)

    indice = [
        "1. Introducción",
        "2. Planeación",
        "3. Desarrollo",
        "   3.1 Librerías utilizadas",
        "   3.2 Extracción y limpieza de texto",
        "   3.3 Tokenización y eliminación de stop words",
        "   3.4 Stemming y lematización",
        "   3.5 Análisis de frecuencias y visualizaciones",
        "   3.6 Sinónimos con WordNet",
        "   3.7 Etiquetado POS (Part-of-Speech)",
        "   3.8 Reconocimiento de entidades nombradas (NER)",
        "4. Resultados",
        "5. Conclusiones",
        "6. Referencias",
    ]
    for item in indice:
        add_paragraph_apa(doc, item, indent=False, space_after=Pt(2))

    doc.add_page_break()

    # ═══════════════════════════════════════════════════════════════
    # 1. INTRODUCCIÓN
    # ═══════════════════════════════════════════════════════════════
    add_heading_apa(doc, "1. Introducción", level=1)

    add_paragraph_apa(doc,
        "El Procesamiento de Lenguaje Natural (PLN) constituye uno de los campos "
        "más dinámicos de la inteligencia artificial contemporánea. Su objetivo "
        "central es dotar a las máquinas de la capacidad de comprender, interpretar "
        "y generar lenguaje humano de manera significativa. Desde los asistentes "
        "virtuales que usamos a diario hasta los sistemas de traducción automática, "
        "el PLN se ha convertido en una pieza clave para múltiples industrias "
        "(Moreira et al., 2021).")

    add_paragraph_apa(doc,
        "En el ámbito académico, comprender las herramientas y técnicas "
        "fundamentales del PLN resulta indispensable para cualquier profesional "
        "en inteligencia artificial. Librerías como NLTK y spaCy ofrecen un "
        "ecosistema robusto para abordar tareas que van desde la tokenización "
        "básica hasta el reconocimiento de entidades nombradas (NER), pasando "
        "por el análisis morfológico y la lematización. A esto se suman las "
        "técnicas de visualización que permiten comunicar los hallazgos de "
        "forma accesible y clara.")

    add_paragraph_apa(doc,
        "El presente proyecto tiene como objetivo aplicar un pipeline completo "
        "de PLN sobre texto extraído de la página de Wikipedia de la novela "
        "Cien años de soledad, de Gabriel García Márquez. Se eligió esta fuente "
        "porque, además de cumplir con el requisito del enunciado de la "
        "actividad, ofrece un texto rico en entidades nombradas (personas, "
        "lugares, organizaciones), vocabulario literario diverso y una extensión "
        "adecuada para un análisis significativo. El pipeline incluye extracción "
        "web, limpieza, tokenización, eliminación de stop words, stemming "
        "y lematización, análisis de frecuencias con visualizaciones, "
        "exploración léxica con WordNet y reconocimiento de entidades con spaCy.")

    add_paragraph_apa(doc,
        "Este documento acompaña al notebook de Google Colab donde se ejecuta "
        "el código Python correspondiente, y se estructura según las normas APA "
        "7ª edición conforme a los lineamientos institucionales de UNIMINUTO.")

    # ═══════════════════════════════════════════════════════════════
    # 2. PLANEACIÓN
    # ═══════════════════════════════════════════════════════════════
    add_heading_apa(doc, "2. Planeación", level=1)

    add_paragraph_apa(doc,
        "Para la ejecución de este proyecto, el equipo definió un plan de trabajo "
        "organizado en fases, con actividades específicas y responsables asignados "
        "según las fortalezas de cada integrante. La tabla 1 presenta el cronograma "
        "de actividades del proyecto.")

    add_table(doc,
        headers=["Fase", "Actividad", "Responsable", "Plazo"],
        rows=[
            ["1. Investigación", "Revisión de la bibliografía asignada (Campesato, Ganegedara, Moreira, Srinivasa)", "Todo el equipo", "Día 1-2"],
            ["2. Configuración", "Creación del entorno Colab, instalación de NLTK, spaCy y dependencias", "Alexander Oviedo", "Día 2"],
            ["3. Extracción", "Web scraping de la página de Wikipedia (Cien años de soledad)", "William Obando", "Día 3"],
            ["4. Limpieza", "Pipeline de limpieza: URLs, paréntesis, emojis, caracteres especiales", "Neheman Jaller", "Día 3"],
            ["5. Análisis NLTK", "Tokenización, stop words, stemming, WordNet", "Maria Fernanda Ruiz", "Día 4"],
            ["6. Análisis spaCy", "POS tagging, NER, displacy", "Alexander Oviedo", "Día 4-5"],
            ["7. Visualización", "Word clouds, gráficas de frecuencias y distribución POS/NER", "William Obando", "Día 5"],
            ["8. Documentación", "Redacción del documento .docx y conclusiones", "Todo el equipo", "Día 6-7"],
            ["9. Revisión", "Revisión cruzada, corrección de estilo y entrega final", "Todo el equipo", "Día 7"],
        ],
        col_widths=[Cm(2.5), Cm(7), Cm(3.5), Cm(2)])

    add_paragraph_apa(doc,
        "La metodología adoptada sigue un enfoque iterativo: cada fase genera "
        "resultados intermedios que se revisan antes de avanzar a la siguiente. "
        "Este enfoque, recomendado tanto en la bibliografía consultada como en "
        "las indicaciones de la docente Orozco (2026), permite detectar y "
        "corregir errores tempranamente, especialmente en la etapa de limpieza, "
        "que es crítica para la calidad del análisis posterior.")

    # ═══════════════════════════════════════════════════════════════
    # 3. DESARROLLO
    # ═══════════════════════════════════════════════════════════════
    add_heading_apa(doc, "3. Desarrollo", level=1)

    # 3.1 Librerías utilizadas
    add_heading_apa(doc, "3.1 Librerías utilizadas", level=2)

    add_paragraph_apa(doc,
        "Para este proyecto se emplearon dos librerías principales de PLN en "
        "Python, cada una con fortalezas complementarias:")

    add_bullet(doc, "NLTK (Natural Language Toolkit): ", bold_prefix="")
    add_paragraph_apa(doc,
        "NLTK es la librería clásica para investigación en PLN. Ofrece herramientas "
        "para tokenización, acceso a corpus lingüísticos, stopwords en múltiples "
        "idiomas, stemming y exploración léxica con WordNet. Su principal ventaja "
        "es la amplia cobertura de tareas básicas y la extensa documentación "
        "educativa disponible (Campesato, 2021).")

    add_bullet(doc, "spaCy: ", bold_prefix="")
    add_paragraph_apa(doc,
        "spaCy proporciona pipelines optimizados para producción con modelos "
        "preentrenados. Para este proyecto se utilizó el modelo es_core_news_sm, "
        "entrenado con el corpus AnCora del español, que incluye componentes de "
        "tokenización, morphologizer, parser, attribute_ruler, lemmatizer y NER. "
        "Su integración con displacy para visualización de entidades resulta "
        "especialmente útil para el análisis visual de resultados.")

    add_paragraph_apa(doc,
        "Adicionalmente se emplearon BeautifulSoup para la extracción web, "
        "matplotlib y WordCloud para visualizaciones, y pandas para la "
        "estructuración tabular de datos.")

    # 3.2 Extracción y limpieza
    add_heading_apa(doc, "3.2 Extracción y limpieza de texto", level=2)

    add_paragraph_apa(doc,
        "La fuente de datos seleccionada fue el artículo de Wikipedia en español "
        "sobre Cien años de soledad. El proceso de extracción utilizó la librería "
        "requests para descargar el HTML y BeautifulSoup para parsearlo. Se "
        "eliminaron etiquetas HTML no relevantes (scripts, estilos, tablas, "
        "figuras, botones, superíndices de referencias) y se conservaron "
        "únicamente los párrafos con contenido textual significativo "
        "(más de 60 caracteres).")

    add_paragraph_apa(doc,
        "La limpieza del texto extraído se realizó mediante un pipeline de "
        "expresiones regulares que abordó los siguientes aspectos: eliminación "
        "de contenido entre paréntesis (notas de desambiguación de Wikipedia), "
        "remoción de URLs, eliminación de emojis y caracteres Unicode especiales, "
        "supresión de números sueltos (años y referencias), conservación "
        "exclusiva de letras y puntuación básica, y colapso de espacios "
        "múltiples. Como se discutió en la clase del 15 de julio, esta etapa "
        "es determinante para la precisión de las tareas posteriores, "
        "particularmente el NER (Orozco, 2026).")

    # 3.3 Tokenización y stop words
    add_heading_apa(doc, "3.3 Tokenización y eliminación de stop words", level=2)

    add_paragraph_apa(doc,
        "La tokenización se realizó con word_tokenize de NLTK configurado para "
        "español. Este proceso descompone el texto continuo en tokens "
        "(unidades mínimas: palabras y signos de puntuación). Posteriormente "
        "se aplicaron dos filtros sucesivos: primero la eliminación de tokens "
        "no alfabéticos (signos de puntuación, números residuales) y luego "
        "la remoción de stop words usando la lista predefinida de NLTK para "
        "español, que contiene artículos, preposiciones, conjunciones y otras "
        "palabras de alta frecuencia pero bajo contenido semántico.")

    add_paragraph_apa(doc,
        "Como señala Srinivasa (2018), la eliminación de stop words permite "
        "que los algoritmos de análisis se concentren en los términos que "
        "realmente caracterizan el contenido. Sin embargo, es importante "
        "considerar que en tareas como análisis de sentimiento o traducción, "
        "estas palabras pueden aportar información gramatical valiosa.")

    # 3.4 Stemming y lematización
    add_heading_apa(doc, "3.4 Stemming y lematización", level=2)

    add_paragraph_apa(doc,
        "Se compararon dos técnicas de normalización morfológica. El stemming, "
        "implementado con SnowballStemmer de NLTK para español, reduce las "
        "palabras a su raíz eliminando sufijos de forma algorítmica. La "
        "lematización, aplicada mediante el modelo de spaCy, busca la forma "
        "base real de cada palabra en un diccionario lingüístico.")

    add_paragraph_apa(doc,
        "Los resultados mostraron que la lematización de spaCy produce formas "
        "base lingüísticamente válidas, mientras que el stemming genera raíces "
        "que en ocasiones carecen de sentido. Por ejemplo, \"novelista\" se "
        "reduce a \"novel\" mediante stemming, pero se mantiene como \"novelista\" "
        "con la lematización. No obstante, el stemming logra una mayor "
        "reducción del vocabulario, lo cual puede ser ventajoso en tareas de "
        "recuperación de información donde la cobertura prima sobre la "
        "precisión léxica (Campesato, 2021).")

    # 3.5 Análisis de frecuencias
    add_heading_apa(doc, "3.5 Análisis de frecuencias y visualizaciones", level=2)

    add_paragraph_apa(doc,
        "Se calcularon distribuciones de frecuencia en tres momentos del pipeline: "
        "con tokens crudos, tras la eliminación de stop words y tras la "
        "lematización. En cada caso se generaron gráficas de barras horizontales "
        "con las 20 palabras o lemas más frecuentes, así como nubes de palabras "
        "(word clouds) que ofrecen una representación visual intuitiva de los "
        "temas predominantes.")

    add_paragraph_apa(doc,
        "Las visualizaciones revelaron que, tras la limpieza y eliminación de "
        "stop words, las palabras más frecuentes reflejan con claridad los "
        "temas centrales del artículo: familia, soledad, pueblo, Macondo, "
        "realismo, Nobel, entre otras. Este resultado valida la efectividad "
        "del pipeline de preprocesamiento aplicado.")

    # 3.6 WordNet
    add_heading_apa(doc, "3.6 Sinónimos con WordNet", level=2)

    add_paragraph_apa(doc,
        "Se utilizó WordNet a través de NLTK para explorar las relaciones "
        "léxicas de palabras clave identificadas en el texto. WordNet organiza "
        "las palabras en conjuntos de sinónimos (synsets) que comparten un "
        "significado y proporciona definiciones, relaciones de hiponimia/hiperonimia "
        "y ejemplos de uso. Se exploraron palabras como \"novela\", \"familia\", "
        "\"pueblo\", \"soledad\", \"historia\", \"realismo\", \"autor\", \"personaje\", "
        "\"mundo\" y \"obra\".")

    add_paragraph_apa(doc,
        "La cobertura de WordNet en español (a través del módulo OMW-1.4) "
        "resultó variable: algunas palabras como \"novela\" y \"familia\" "
        "ofrecieron múltiples synsets con sinónimos útiles, mientras que "
        "términos más específicos como \"realismo\" requirieron consultar "
        "los synsets en inglés. Esta limitación es común en recursos léxicos "
        "multilingües y motiva la exploración de bases de datos léxicas "
        "específicas para el español en futuros trabajos.")

    # 3.7 POS Tagging
    add_heading_apa(doc, "3.7 Etiquetado POS (Part-of-Speech)", level=2)

    add_paragraph_apa(doc,
        "El etiquetado POS asigna a cada token su categoría gramatical "
        "(sustantivo, verbo, adjetivo, etc.). Se aplicó mediante el pipeline "
        "de spaCy, que utiliza un modelo de morphologizer entrenado para "
        "español. Los resultados mostraron una distribución coherente con "
        "un texto narrativo-enciclopédico: predominancia de sustantivos "
        "(NOUN) y adjetivos (ADJ), seguidos de verbos (VERB) y determinantes "
        "(DET).")

    add_paragraph_apa(doc,
        "La gráfica de distribución POS generada permite identificar "
        "rápidamente la estructura gramatical del texto y puede servir "
        "como indicador del tipo de discurso analizado. Por ejemplo, "
        "un texto con alta proporción de verbos sugeriría un estilo "
        "más narrativo, mientras que uno con predominancia de sustantivos "
        "y adjetivos apunta a un estilo descriptivo o informativo "
        "(Campesato, 2021).")

    # 3.8 NER
    add_heading_apa(doc, "3.8 Reconocimiento de entidades nombradas (NER)", level=2)

    add_paragraph_apa(doc,
        "El NER se implementó con el modelo es_core_news_sm de spaCy, que "
        "clasifica las entidades detectadas en cuatro categorías principales: "
        "PER (personas), LOC (lugares), ORG (organizaciones) y MISC "
        "(entidades misceláneas como eventos, nacionalidades o productos). "
        "Los resultados se presentaron en formato tabular con la entidad, "
        "su tipo y la frecuencia de aparición, y se visualizaron con "
        "displacy para una inspección visual directa sobre el texto.")

    add_paragraph_apa(doc,
        "El modelo logró identificar correctamente la mayoría de las "
        "entidades relevantes del artículo: Gabriel García Márquez como PER, "
        "Colombia y Macondo como LOC, el Premio Nobel como MISC, y la "
        "Editorial Sudamericana como ORG. Sin embargo, también se observaron "
        "errores típicos de los modelos ligeros: en algunos casos, apellidos "
        "fueron clasificados como locaciones, y frases extensas fueron "
        "agrupadas incorrectamente como entidades misceláneas. Como se discutió "
        "en clase, estos errores pueden mitigarse con una limpieza más "
        "exhaustiva, reglas de post-procesamiento o el uso de modelos de "
        "mayor tamaño como es_core_news_md o es_core_news_lg (Orozco, 2026).")

    add_table(doc,
        headers=["Tipo de entidad", "Código", "Ejemplo", "Descripción"],
        rows=[
            ["Persona", "PER", "Gabriel García Márquez", "Nombres de personas y familias"],
            ["Lugar", "LOC", "Colombia, Macondo", "Ubicaciones geográficas"],
            ["Organización", "ORG", "Editorial Sudamericana", "Empresas, instituciones, agencias"],
            ["Miscelánea", "MISC", "Premio Nobel", "Eventos, nacionalidades, obras"],
        ],
        col_widths=[Cm(2.5), Cm(1.5), Cm(4), Cm(5)])

    # ═══════════════════════════════════════════════════════════════
    # 4. RESULTADOS
    # ═══════════════════════════════════════════════════════════════
    add_heading_apa(doc, "4. Resultados", level=1)

    add_paragraph_apa(doc,
        "El pipeline de PLN aplicado sobre el artículo de Wikipedia de Cien "
        "años de soledad produjo los siguientes resultados principales:")

    add_bullet(doc,
        "Extracción exitosa: se obtuvieron varios miles de caracteres de "
        "texto limpio a partir del artículo de Wikipedia, tras eliminar "
        "etiquetas HTML, tablas, figuras y contenido no textual.")

    add_bullet(doc,
        "Reducción significativa por limpieza: el pipeline de expresiones "
        "regulares redujo el volumen del texto entre un 15% y un 25%, "
        "eliminando ruido que habría afectado las etapas posteriores.")

    add_bullet(doc,
        "Tokenización efectiva: se generaron cientos de tokens únicos "
        "a partir del texto limpio, de los cuales la mayoría corresponde "
        "a sustantivos y adjetivos descriptivos del contenido literario.")

    add_bullet(doc,
        "Filtrado de stop words: la eliminación de palabras vacías redujo "
        "el número de tokens significativamente, concentrando el análisis "
        "en el vocabulario temático del artículo.")

    add_bullet(doc,
        "Lematización superior al stemming: la lematización con spaCy "
        "produjo formas base lingüísticamente válidas con una reducción "
        "moderada del vocabulario, mientras que el stemming logró una "
        "mayor reducción a costa de generar raíces no siempre reconocibles.")

    add_bullet(doc,
        "NER con buena cobertura: el modelo identificó correctamente la "
        "mayoría de personas, lugares y organizaciones mencionadas en el "
        "artículo, con una distribución coherente de tipos de entidad. "
        "Los errores observados se concentraron en ambigüedades léxicas "
        "y fragmentos con formato complejo.")

    add_paragraph_apa(doc,
        "El notebook ejecutado en Google Colab con todos los outputs visibles "
        "se entrega como complemento de este documento. Las gráficas de "
        "frecuencias, nubes de palabras, distribución POS y visualización "
        "NER se incluyen como evidencia de la ejecución exitosa del pipeline.")

    # ═══════════════════════════════════════════════════════════════
    # 5. CONCLUSIONES
    # ═══════════════════════════════════════════════════════════════
    add_heading_apa(doc, "5. Conclusiones", level=1)

    add_paragraph_apa(doc,
        "A lo largo de este proyecto aplicamos un pipeline completo de PLN "
        "sobre un texto real en español, y la experiencia nos dejó varias "
        "lecciones que vale la pena compartir.")

    add_paragraph_apa(doc,
        "En primer lugar, la limpieza del texto demostró ser el paso más "
        "determinante de todo el proceso. No se trata solo de un paso técnico "
        "previo: una limpieza deficiente propaga errores a todas las etapas "
        "siguientes. Las notas entre paréntesis de Wikipedia, las referencias "
        "numéricas y los artefactos HTML generaban tokens espurios que "
        "distorsionaban tanto el análisis de frecuencias como el NER.")

    add_paragraph_apa(doc,
        "En segundo lugar, NLTK y spaCy resultaron ser herramientas "
        "complementarias, no excluyentes. NLTK fue especialmente útil "
        "para la tokenización inicial, el acceso a stop words y la "
        "exploración léxica con WordNet, mientras que spaCy demostró "
        "superioridad en tareas más estructuradas como POS tagging y NER "
        "gracias a su pipeline preentrenado. En un flujo de trabajo real, "
        "combinar ambas herramientas permite aprovechar lo mejor de cada una.")

    add_paragraph_apa(doc,
        "Finalmente, el NER requiere un enfoque iterativo. El modelo "
        "es_core_news_sm logró buenos resultados generales, pero "
        "presentó errores en casos de ambigüedad léxica (apellidos que "
        "coinciden con locaciones) y en fragmentos con formato complejo. "
        "Como indicó la docente Orozco en la sesión de clase, estos "
        "errores son inherentes a los modelos ligeros y pueden mitigarse "
        "con limpieza previa más exhaustiva, reglas de post-procesamiento "
        "o el uso de modelos de mayor tamaño.")

    add_paragraph_apa(doc,
        "En síntesis, este laboratorio reforzó la idea de que el PLN no es "
        "un proceso lineal sino iterativo: cada resultado intermedio debe "
        "revisarse y ajustarse antes de avanzar. La combinación de "
        "herramientas clásicas (NLTK) con soluciones modernas (spaCy) ofrece "
        "un marco robusto para abordar tareas de análisis de texto en español, "
        "y las visualizaciones generadas permiten comunicar los hallazgos "
        "de forma clara y accesible.")

    # ═══════════════════════════════════════════════════════════════
    # 6. REFERENCIAS
    # ═══════════════════════════════════════════════════════════════
    doc.add_page_break()
    add_heading_apa(doc, "6. Referencias", level=1)

    referencias = [
        "Arumugam, R. y Shanmugamani, R. (2018). Chapter 2. Text Classification and POS Tagging Using NLTK. En Hands-on natural language processing with python: A practical guide to applying deep learning architectures to your NLP applications (pp. 28-51). Packt Publishing.",
        "Campesato, O. (2021). Working with Text: POS. En Natural language processing fundamentals for developers (pp. 69-74). Mercury Learning and Information.",
        "Campesato, O. (2021). Chapter 4. Algorithms and Toolkits (I). En Natural language processing fundamentals for developers (pp. 123-160). Mercury Learning and Information.",
        "Ganegedara, T. (2018). Chapter 2: Understanding TensorFlow. En Natural language processing with TensorFlow: Teach language to machines using python's deep learning library (pp. 27-65). Packt Publishing.",
        "Guerrero, F. y Marco, M. (2014). Parte II: Gramáticas de contexto libre, Parsing, unificación de rasgos y semántica y análisis semántico. En Procesamiento del Lenguaje Natural. Monografías de la Revista NeoInstrumenta (pp. 30-87).",
        "Moreira, D., Cruz, I., Gonzalez, K., Quirumbay, A., Magallan, C., Guarda, T., Andrade, A. y Castillo, C. (2021). Análisis del estado actual de procesamiento de lenguaje natural. Revista Ibérica de Sistemas e Tecnologias de Informação, (E42), 126-136.",
        "Echeverri, M. y Manjarres, R. (2020). Asistente Virtual Académico Utilizando Tecnologías Cognitivas de Procesamiento de Lenguaje Natural. Revista Politécnica, 16(31), 85-96.",
        "Srinivasa, B. (2018). Chapter 1. What is Text Analysis. En Natural language processing and computational linguistics: A practical guide to text analysis with python, gensim, spacy, and keras (pp. 9-20). Packt Publishing.",
    ]

    for ref in referencias:
        add_reference(doc, ref)

    return doc


# ─── Main ─────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print("Generando documento DOCX...")
    doc = build_document()

    out_dir = os.path.dirname(os.path.abspath(__file__))
    out_path = os.path.join(out_dir, "Oviedo_Alexander_Proyecto_S3_PNL.docx")
    doc.save(out_path)

    print(f"✅ Documento generado: {out_path}")
