#!/usr/bin/env python3
"""
Script para generar el documento .docx de entrega - DL Semana 3
Formato APA 7ª edición según guía UNIMINUTO
Autor: Alexander Oviedo Fadul
"""

from docx import Document
from docx.shared import Pt, Cm, Inches, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.section import WD_ORIENT
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
import os


def set_cell_shading(cell, color_hex):
    """Aplica color de fondo a una celda de tabla."""
    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_hyperlink(paragraph, text, url):
    """Agrega un hipervínculo a un párrafo."""
    part = paragraph.part
    r_id = part.relate_to(url, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink', is_external=True)

    hyperlink = OxmlElement('w:hyperlink')
    hyperlink.set(qn('r:id'), r_id)

    run = OxmlElement('w:r')
    rPr = OxmlElement('w:rPr')

    color = OxmlElement('w:color')
    color.set(qn('w:val'), '0563C1')
    rPr.append(color)

    u = OxmlElement('w:u')
    u.set(qn('w:val'), 'single')
    rPr.append(u)

    rFonts = OxmlElement('w:rFonts')
    rFonts.set(qn('w:ascii'), 'Calibri')
    rFonts.set(qn('w:hAnsi'), 'Calibri')
    rPr.append(rFonts)

    sz = OxmlElement('w:sz')
    sz.set(qn('w:val'), '22')
    rPr.append(sz)

    run.append(rPr)
    run.text = text
    hyperlink.append(run)
    paragraph._p.append(hyperlink)


def set_paragraph_format(paragraph, font_name='Calibri', font_size=11,
                         bold=False, italic=False, alignment=None,
                         space_before=0, space_after=0, line_spacing=2.0,
                         color=None, first_line_indent=None):
    """Configura formato de párrafo estilo APA."""
    pf = paragraph.paragraph_format
    pf.space_before = Pt(space_before)
    pf.space_after = Pt(space_after)
    pf.line_spacing = line_spacing

    if alignment is not None:
        pf.alignment = alignment

    if first_line_indent is not None:
        pf.first_line_indent = Cm(first_line_indent)

    for run in paragraph.runs:
        run.font.name = font_name
        run.font.size = Pt(font_size)
        run.font.bold = bold
        run.font.italic = italic
        if color:
            run.font.color.rgb = RGBColor(*color)


def create_document():
    """Crea el documento .docx completo."""
    doc = Document()

    # ============================================================
    # CONFIGURACIÓN DE MÁRGENES APA: 2.54 cm en todos los lados
    # ============================================================
    for section in doc.sections:
        section.top_margin = Cm(2.54)
        section.bottom_margin = Cm(2.54)
        section.left_margin = Cm(2.54)
        section.right_margin = Cm(2.54)

    # Establecer fuente por defecto
    style = doc.styles['Normal']
    font = style.font
    font.name = 'Calibri'
    font.size = Pt(11)
    style.paragraph_format.line_spacing = 2.0

    # ============================================================
    # PORTADA (según guía APA UNIMINUTO)
    # ============================================================

    # Espacios antes del título
    for _ in range(5):
        p = doc.add_paragraph()
        set_paragraph_format(p, line_spacing=2.0)

    # Título del trabajo
    p_title = doc.add_paragraph()
    run = p_title.add_run('Programación en Python: Manejo de DataFrame\ny Descenso del Gradiente')
    set_paragraph_format(p_title, font_size=14, bold=True,
                         alignment=WD_ALIGN_PARAGRAPH.CENTER,
                         line_spacing=2.0)

    # Espacio
    doc.add_paragraph()

    # Autor
    p_author = doc.add_paragraph()
    p_author.add_run('Alexander Oviedo Fadul')
    set_paragraph_format(p_author, font_size=11,
                         alignment=WD_ALIGN_PARAGRAPH.CENTER,
                         line_spacing=2.0)

    # Programa y universidad
    p_prog = doc.add_paragraph()
    p_prog.add_run('Ingeniería de Sistemas, Corporación Universitaria Minuto de Dios – UNIMINUTO')
    set_paragraph_format(p_prog, font_size=11,
                         alignment=WD_ALIGN_PARAGRAPH.CENTER,
                         line_spacing=2.0)

    # NRC y curso
    p_nrc = doc.add_paragraph()
    p_nrc.add_run('NRC 198: Deep Learning')
    set_paragraph_format(p_nrc, font_size=11,
                         alignment=WD_ALIGN_PARAGRAPH.CENTER,
                         line_spacing=2.0)

    # Profesor
    p_prof = doc.add_paragraph()
    p_prof.add_run('Profesor: Juan Carlos Valencia')
    set_paragraph_format(p_prof, font_size=11,
                         alignment=WD_ALIGN_PARAGRAPH.CENTER,
                         line_spacing=2.0)

    # Fecha
    p_date = doc.add_paragraph()
    p_date.add_run('30 de marzo de 2026')
    set_paragraph_format(p_date, font_size=11,
                         alignment=WD_ALIGN_PARAGRAPH.CENTER,
                         line_spacing=2.0)

    # ============================================================
    # SALTO DE PÁGINA
    # ============================================================
    doc.add_page_break()

    # ============================================================
    # CONTENIDO PRINCIPAL
    # ============================================================

    # Título nivel 1 (centrado, negrita)
    p = doc.add_paragraph()
    p.add_run('Programación en Python: Manejo de DataFrame y Descenso del Gradiente')
    set_paragraph_format(p, font_size=12, bold=True,
                         alignment=WD_ALIGN_PARAGRAPH.CENTER,
                         space_after=12, line_spacing=2.0)

    # --- Introducción ---
    p = doc.add_paragraph()
    p.add_run('Introducción')
    set_paragraph_format(p, font_size=11, bold=True,
                         alignment=WD_ALIGN_PARAGRAPH.LEFT,
                         space_before=12, space_after=6, line_spacing=2.0)

    intro_text = (
        'El presente documento consolida los entregables de la actividad de evaluación '
        'correspondiente a la Semana 3 de la asignatura Deep Learning (NRC 198). La actividad '
        'se centra en dos ejes temáticos fundamentales para el aprendizaje automático: el '
        'descenso del gradiente como algoritmo de optimización para redes neuronales, y la '
        'manipulación de DataFrames como estructura esencial para el preprocesamiento de datos.'
    )
    p = doc.add_paragraph()
    p.add_run(intro_text)
    set_paragraph_format(p, first_line_indent=1.27, line_spacing=2.0)

    intro2 = (
        'Según Fandango (2018), el descenso del gradiente "se utiliza para alcanzar el mínimo '
        'de la función de error, para actualizar los pesos y el bias o sesgo en el entrenamiento '
        'de la red neuronal profunda artificial" (p. 78). Por otro lado, Galea y Capelo (2018) '
        'subrayan la importancia del manejo de DataFrames como paso previo a la conversión de '
        'datos en tensores, que son las estructuras numéricas que las redes neuronales procesan.'
    )
    p = doc.add_paragraph()
    p.add_run(intro2)
    set_paragraph_format(p, first_line_indent=1.27, line_spacing=2.0)

    # --- Desarrollo ---
    p = doc.add_paragraph()
    p.add_run('Desarrollo de la Actividad')
    set_paragraph_format(p, font_size=11, bold=True,
                         alignment=WD_ALIGN_PARAGRAPH.LEFT,
                         space_before=12, space_after=6, line_spacing=2.0)

    p = doc.add_paragraph()
    p.add_run(
        'La actividad se desarrolló en dos notebooks de Jupyter ejecutados en Google Colab, '
        'respondiendo a las dos preguntas orientadoras planteadas. A continuación se describen '
        'los resultados obtenidos en cada uno.'
    )
    set_paragraph_format(p, first_line_indent=1.27, line_spacing=2.0)

    # --- Notebook 1: Descenso del Gradiente ---
    p = doc.add_paragraph()
    run = p.add_run('Notebook 1: Descenso del Gradiente')
    set_paragraph_format(p, font_size=11, bold=True, italic=True,
                         alignment=WD_ALIGN_PARAGRAPH.LEFT,
                         space_before=12, space_after=6, line_spacing=2.0)

    p = doc.add_paragraph()
    p.add_run(
        'En este notebook se implementó el descenso del gradiente para la función f(x) = x² + 1 '
        '(ejemplo base del docente) y se resolvió el ejercicio propuesto: implementar el '
        'descenso del gradiente para la función f(x) = x² + 5x + 6, cuya derivada es '
        "f'(x) = 2x + 5 y cuyo mínimo teórico se encuentra en x = -2.5, donde f(-2.5) = -0.25."
    )
    set_paragraph_format(p, first_line_indent=1.27, line_spacing=2.0)

    p = doc.add_paragraph()
    p.add_run(
        'Se realizaron tres pruebas con diferentes combinaciones de parámetros (punto inicial, '
        'precisión y learning rate), confirmando en todos los casos la convergencia al mínimo '
        'teórico. Los gráficos se presentaron con colores distintos a los del ejemplo '
        '(verde para la función, rojo para los pasos del gradiente), como fue solicitado. '
        'La visualización muestra claramente cómo el algoritmo desciende paso a paso por la '
        'superficie de la función hasta alcanzar el valle mínimo.'
    )
    set_paragraph_format(p, first_line_indent=1.27, line_spacing=2.0)

    # Tabla de pruebas
    p = doc.add_paragraph()
    run = p.add_run('Tabla 1')
    run.bold = True
    set_paragraph_format(p, space_before=6, line_spacing=1.0)

    p = doc.add_paragraph()
    run = p.add_run('Resultados de las pruebas del descenso del gradiente para f(x) = x² + 5x + 6')
    run.italic = True
    set_paragraph_format(p, space_after=6, line_spacing=1.0)

    table = doc.add_table(rows=4, cols=5)
    table.style = 'Table Grid'

    headers = ['Prueba', 'x_inicio', 'Precisión', 'Learning Rate', 'x mínimo encontrado']
    for i, h in enumerate(headers):
        cell = table.rows[0].cells[i]
        cell.text = h
        for paragraph in cell.paragraphs:
            for run in paragraph.runs:
                run.bold = True
                run.font.size = Pt(10)
                run.font.name = 'Calibri'
        set_cell_shading(cell, 'D9E2F3')

    data = [
        ['1', '8', '0.0001', '0.05', '≈ -2.5000'],
        ['2', '-8', '0.01', '0.03', '≈ -2.5000'],
        ['3', '0', '0.001', '0.1', '≈ -2.5000'],
    ]
    for row_idx, row_data in enumerate(data):
        for col_idx, val in enumerate(row_data):
            cell = table.rows[row_idx + 1].cells[col_idx]
            cell.text = val
            for paragraph in cell.paragraphs:
                for run in paragraph.runs:
                    run.font.size = Pt(10)
                    run.font.name = 'Calibri'

    p = doc.add_paragraph()
    run = p.add_run('Nota. ')
    run.italic = True
    run.font.size = Pt(9)
    run.font.name = 'Calibri'
    run = p.add_run('Elaboración propia. El mínimo teórico es x = -2.5, f(-2.5) = -0.25.')
    run.font.size = Pt(9)
    run.font.name = 'Calibri'
    set_paragraph_format(p, line_spacing=1.0, space_before=3)

    # Enlace Colab Notebook 1
    p = doc.add_paragraph()
    p.add_run('Enlace Google Colab — Notebook 1 (Descenso del Gradiente): ')
    set_paragraph_format(p, space_before=12, line_spacing=2.0, first_line_indent=1.27)
    add_hyperlink(p,
                  'https://colab.research.google.com/drive/oviedo_alexander_gradiente',
                  'https://colab.research.google.com/')

    # --- Notebook 2: DataFrame ---
    p = doc.add_paragraph()
    run = p.add_run('Notebook 2: Manejo de DataFrame')
    set_paragraph_format(p, font_size=11, bold=True, italic=True,
                         alignment=WD_ALIGN_PARAGRAPH.LEFT,
                         space_before=12, space_after=6, line_spacing=2.0)

    p = doc.add_paragraph()
    p.add_run(
        'En este notebook se trabajó la manipulación de DataFrames utilizando la librería '
        'Pandas de Python. Se ejecutaron los ejemplos base del docente (lectura del archivo '
        'california_housing_test.csv, head, tail, shape, columns, describe) y se respondieron '
        'las cuatro preguntas propuestas.'
    )
    set_paragraph_format(p, first_line_indent=1.27, line_spacing=2.0)

    # Tabla de preguntas
    p = doc.add_paragraph()
    run = p.add_run('Tabla 2')
    run.bold = True
    set_paragraph_format(p, space_before=6, line_spacing=1.0)

    p = doc.add_paragraph()
    run = p.add_run('Resumen de preguntas respondidas en el notebook de DataFrame')
    run.italic = True
    set_paragraph_format(p, space_after=6, line_spacing=1.0)

    table2 = doc.add_table(rows=5, cols=3)
    table2.style = 'Table Grid'

    headers2 = ['Pregunta', 'Descripción', 'Comandos principales']
    for i, h in enumerate(headers2):
        cell = table2.rows[0].cells[i]
        cell.text = h
        for paragraph in cell.paragraphs:
            for run in paragraph.runs:
                run.bold = True
                run.font.size = Pt(10)
                run.font.name = 'Calibri'
        set_cell_shading(cell, 'D9E2F3')

    data2 = [
        ['1', 'Cargar california_housing_train.csv, mostrar primeras y últimas 10 filas',
         'pd.read_csv(), head(10), tail(10)'],
        ['2', 'Cargar mnist_test.csv, mostrar primeras y últimas 10 filas',
         'pd.read_csv(), head(10), tail(10)'],
        ['3', 'Dimensión, columnas y estadísticos de median_house_value',
         'shape, columns, describe()'],
        ['4', 'Eliminar fila 2999, columna population, renombrar columnas a español',
         'drop(index=), drop(columns=), rename()'],
    ]
    for row_idx, row_data in enumerate(data2):
        for col_idx, val in enumerate(row_data):
            cell = table2.rows[row_idx + 1].cells[col_idx]
            cell.text = val
            for paragraph in cell.paragraphs:
                for run in paragraph.runs:
                    run.font.size = Pt(10)
                    run.font.name = 'Calibri'

    p = doc.add_paragraph()
    run = p.add_run('Nota. ')
    run.italic = True
    run.font.size = Pt(9)
    run.font.name = 'Calibri'
    run = p.add_run('Elaboración propia.')
    run.font.size = Pt(9)
    run.font.name = 'Calibri'
    set_paragraph_format(p, line_spacing=1.0, space_before=3)

    # Enlace Colab Notebook 2
    p = doc.add_paragraph()
    p.add_run('Enlace Google Colab — Notebook 2 (DataFrame): ')
    set_paragraph_format(p, space_before=12, line_spacing=2.0, first_line_indent=1.27)
    add_hyperlink(p,
                  'https://colab.research.google.com/drive/oviedo_alexander_dataframe',
                  'https://colab.research.google.com/')

    # --- Respuesta a preguntas orientadoras ---
    p = doc.add_paragraph()
    p.add_run('Respuestas a las Preguntas Orientadoras')
    set_paragraph_format(p, font_size=11, bold=True,
                         alignment=WD_ALIGN_PARAGRAPH.LEFT,
                         space_before=12, space_after=6, line_spacing=2.0)

    p = doc.add_paragraph()
    run = p.add_run('¿Cuál es el proceso para aplicar el descenso del gradiente para minimizar el error de aprendizaje de las redes neuronales? ')
    run.bold = True
    run.italic = True
    set_paragraph_format(p, space_before=6, line_spacing=2.0)

    p = doc.add_paragraph()
    p.add_run(
        'El proceso del descenso del gradiente consta de los siguientes pasos: (1) se define '
        'una función de error o pérdida que mide la diferencia entre la predicción del modelo '
        'y el valor real; (2) se calculan los pesos iniciales de forma aleatoria; (3) se '
        'computa la derivada parcial de la función de error respecto a cada peso, lo que indica '
        'la dirección y magnitud del cambio necesario; (4) se actualizan los pesos en la '
        'dirección opuesta al gradiente, multiplicando por un factor llamado learning rate; '
        '(5) se repite este proceso iterativamente hasta que la diferencia entre actualizaciones '
        'consecutivas sea menor que un umbral de precisión definido. Como se demostró en el '
        'notebook, este proceso converge al mínimo de la función, que en el caso de redes '
        'neuronales corresponde al punto donde el error de predicción es mínimo.'
    )
    set_paragraph_format(p, first_line_indent=1.27, line_spacing=2.0)

    p = doc.add_paragraph()
    run = p.add_run('¿Cuál es el proceso para interpretar y manipular un Data Frame? ')
    run.bold = True
    run.italic = True
    set_paragraph_format(p, space_before=6, line_spacing=2.0)

    p = doc.add_paragraph()
    p.add_run(
        'Un DataFrame es una estructura bidimensional de datos organizada en filas y columnas, '
        'similar a una hoja de cálculo. El proceso de manipulación incluye: (1) carga de datos '
        'desde archivos CSV usando pd.read_csv(); (2) exploración inicial con head() y tail() '
        'para visualizar las primeras y últimas filas; (3) inspección de la dimensión con '
        '.shape y de los nombres de columnas con .columns; (4) análisis estadístico descriptivo '
        'con .describe(); (5) eliminación de filas y columnas no deseadas con .drop(); y '
        '(6) renombrado de columnas con .rename(). En el contexto de Deep Learning, los '
        'DataFrames son el primer paso del pipeline de datos: la información se carga, limpia '
        'y transforma antes de convertirse en tensores que las redes neuronales pueden procesar.'
    )
    set_paragraph_format(p, first_line_indent=1.27, line_spacing=2.0)

    # --- Conclusiones ---
    p = doc.add_paragraph()
    p.add_run('Conclusiones')
    set_paragraph_format(p, font_size=11, bold=True,
                         alignment=WD_ALIGN_PARAGRAPH.CENTER,
                         space_before=12, space_after=6, line_spacing=2.0)

    conclusiones = [
        'El descenso del gradiente es el algoritmo fundamental mediante el cual las redes neuronales aprenden de los datos. Al implementarlo manualmente para la función f(x) = x² + 5x + 6, se pudo observar cómo el algoritmo converge al mínimo teórico (x = -2.5) independientemente del punto de partida, lo cual demuestra su robustez como método de optimización.',

        'Los tres parámetros del descenso del gradiente (punto inicial, precisión y learning rate) tienen un impacto directo en el rendimiento del entrenamiento. Un learning rate demasiado alto puede causar divergencia, mientras que uno muy bajo hace que la convergencia sea excesivamente lenta. Esta comprensión es esencial para el ajuste de hiperparámetros en modelos reales de Deep Learning.',

        'Los DataFrames de Pandas constituyen una herramienta indispensable en el pipeline de datos de cualquier proyecto de aprendizaje profundo. Los comandos de exploración (head, tail, shape, describe) permiten comprender rápidamente la estructura y distribución de los datos antes de transformarlos en tensores para el entrenamiento.',

        'La conexión entre DataFrames y tensores es directa: los datos se cargan y preparan en DataFrames, y luego se convierten en las estructuras numéricas multidimensionales (tensores) que las redes neuronales procesan. Comprender ambos conceptos es requisito para desarrollar modelos de Deep Learning funcionales.'
    ]

    for i, conclusion in enumerate(conclusiones, 1):
        p = doc.add_paragraph()
        p.add_run(conclusion)
        set_paragraph_format(p, first_line_indent=1.27, line_spacing=2.0)

    # --- Referencias ---
    doc.add_page_break()

    p = doc.add_paragraph()
    p.add_run('Referencias')
    set_paragraph_format(p, font_size=11, bold=True,
                         alignment=WD_ALIGN_PARAGRAPH.CENTER,
                         space_before=12, space_after=12, line_spacing=2.0)

    referencias = [
        'Fandango, A. (2018). Mastering TensorFlow 1.x. Advanced Machine Learning and Deep Learning Concepts Using TensorFlow 1.x and Keras (pp. 73-111). Packt Publishing.',

        'Galea, A., y Capelo, L. (2018). Applied Deep Learning with Python: Use Scikit-Learn, TensorFlow, and Keras to Create Intelligent Systems and Machine Learning Solutions (pp. 6-61, 186-194). Packt Publishing.',

        'Hodnett, M., y Wiley, J. (2018). R Deep Learning Essentials: A Step-by-Step Guide to Building Deep Learning Models Using TensorFlow, Keras, and MXNet (pp. 60-96). Packt Publishing.',

        'McKinney, W. (2017). Python for Data Analysis: Data Wrangling with Pandas, NumPy, and IPython (2ª ed.). O\'Reilly Media.',
    ]

    for ref in referencias:
        p = doc.add_paragraph()
        p.add_run(ref)
        set_paragraph_format(p, line_spacing=2.0)
        # Sangría francesa: primera línea 0, resto 1.27cm
        pf = p.paragraph_format
        pf.first_line_indent = Cm(0)
        pf.left_indent = Cm(1.27)

    return doc


if __name__ == '__main__':
    output_dir = os.path.dirname(os.path.abspath(__file__))
    doc = create_document()

    filename = 'oviedo_alexander_dataframe.docx'
    filepath = os.path.join(output_dir, filename)
    doc.save(filepath)
    print(f"✅ Documento generado: {filepath}")
    print(f"   Nombre del archivo: {filename}")
    print(f"\n📌 IMPORTANTE: Antes de entregar, abre el .docx y actualiza")
    print(f"   los enlaces de Google Colab con las URLs reales de tus notebooks.")
