package com.escolapios.fctmongo.service;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow; // 👈 Corregido import de Word XML
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import com.escolapios.fctmongo.dto.GenerarDocumentoContext;
import com.escolapios.fctmongo.entity.Convenio;
import com.escolapios.fctmongo.entity.ResultadoAprendizaje;
import com.escolapios.fctmongo.service.processor.Anexo6Processor;
import com.escolapios.fctmongo.service.processor.Anexo8Processor;
import com.escolapios.fctmongo.service.processor.Anexo9Processor;
import com.escolapios.fctmongo.service.processor.AnexoProcessor;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor 
public class TemplateService {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy");
    @Autowired
    private Map<String, AnexoProcessor> processors;
        public XWPFDocument procesarDocumentoEnMemoria(GenerarDocumentoContext ctx) throws IOException {
        System.out.println("\n  --- [INICIO: TemplateService en Memoria] ---");      
        ClassPathResource resource = new ClassPathResource("plantillas/" + ctx.plantilla());
        XWPFDocument document = new XWPFDocument(resource.getInputStream());
        Convenio dto = ctx.datos();
        // ⚡ CLAVE LOMBOK: Forzamos el aplanamiento de variables planos {{ ... }} en la raíz antes de operar
        if (dto != null) {
            dto.sincronizarCamposPlanosTemplate(); 
        }
        Map<String, Object> data = new HashMap<>();
        // 🟢 PASO MAESTRO: Metemos el Word vivo de Apache POI en el mapa para alimentar al 
        data.put("DOCUMENT_RAW_POI", document);
        if (dto != null) {
            data.put("numeroAcuerdo", dto.getNumeroAcuerdo() != null ? dto.getNumeroAcuerdo() : "");
            data.put("cursoAcademico", dto.getCursoAcademico() != null ? dto.getCursoAcademico() : ""); 
            data.put("fechaFirma", dto.getFechaFirma() != null ? dto.getFechaFirma().format(FORMATTER) : "");
        }
        data.put("fechaHoy", LocalDate.now().format(FORMATTER));

        // BÚSQUEDA ROBUSTA DEL PROCESADOR DE ANEXOS
        String tipo = ctx.tipoDocumento().toUpperCase().trim();
        AnexoProcessor processor = null;
        for (AnexoProcessor p : processors.values()) {
            if (p.getTipoDocumento() != null && p.getTipoDocumento().toUpperCase().trim().equals(tipo)) {
                processor = p;
                break;
            }
        }
        if (processor != null) {
            System.out.println("  [POI] Ejecutando procesador específico: " + tipo);
            processor.procesarDatosEspecificos(ctx, data);
        } else {
            System.out.println("  [POI] ⚠️ ATENCIÓN: No hay procesador específico registrado para: " + tipo);
        }

        // 🟢 LIMPIEZA ATÓMICA: Borramos la referencia del Word del mapa para que no intente escribirse como texto
        data.remove("DOCUMENT_RAW_POI");

        // IMPRIMIR MAPA PARA CONTROL DE AUDITORÍA
        System.out.println("  [POI] Mapa 'data' final que se intentará escribir en las etiquetas:");
        data.forEach((k, v) -> System.out.println("    └─ {{ " + k + " }} => " + v));

        System.out.println("  [POI] Iniciando reemplazo sobre el esqueleto del Word...");
        replaceText(document, data);
        System.out.println("  --- [FIN: TemplateService en Memoria] ---\n");
        
        return document;
    }
     
    /**
     * 📋 ANEXO 4 MEMORIA: Procesa de forma aislada y exclusiva el Anexo 4.
     * Extrae de forma atómica las ráfagas multi-registro de profesores, tutores y alumnos de MongoDB.
     */
    public XWPFDocument procesarDocumentoEnMemoriaAnexo4(GenerarDocumentoContext ctx) throws IOException {
        System.out.println("\n========================================================================");
        System.out.println("🚨 [PROCESAMIENTO AISLADO: ANEXO 4] - EXTRACCIÓN DIRECTA MONGODB");
        System.out.println("========================================================================");
        
        ClassPathResource resource = new ClassPathResource("plantillas/" + ctx.plantilla());
        XWPFDocument document = new XWPFDocument(resource.getInputStream());

        Convenio dto = ctx.datos();
        Map<String, Object> data = new HashMap<>();

        // Rellenamos metadatos básicos de cabecera
        data.put("fechaHoy", LocalDate.now().format(FORMATTER));
        
        if (dto != null) {
            // Forzamos la sincronización preventiva de Lombok
            dto.sincronizarCamposPlanosTemplate();

            data.put("numeroAcuerdo", dto.getNumeroAcuerdo() != null ? dto.getNumeroAcuerdo() : "");
            data.put("cursoAcademico", dto.getCursoAcademico() != null ? dto.getCursoAcademico() : "");
            data.put("direccion", dto.getEmpresaDireccion() != null ? dto.getEmpresaDireccion() : "");
            data.put("familiaProfesional", dto.getFamiliaProfesionalNombreStr() != null ? dto.getFamiliaProfesionalNombreStr() : "");
            data.put("cicloFormativo", dto.getAlumnoCiclo() != null ? dto.getAlumnoCiclo() : "-");
            data.put("fechaFirma", dto.getFechaFirma() != null ? dto.getFechaFirma().format(FORMATTER) : "");

            // 🟢 EXTRACCIÓN QUIRÚRGICA DEL PROFESOR (Índice 0 del Array de MongoDB)
            if (dto.getProfesores() != null && !dto.getProfesores().isEmpty()) {
                var prof = dto.getProfesores().get(0);
                if (prof != null) {
                    System.out.println("  -> 👨‍🏫 Profesor Extraído: " + prof.getNombre() + " " + prof.getApellidos());
                    data.put("nombreProfesor", prof.getNombre() != null ? prof.getNombre().trim() : "");
                    data.put("apellidosProfesor", prof.getApellidos() != null ? prof.getApellidos().trim() : "");
                    data.put("dniProfesor", prof.getDni() != null ? prof.getDni().trim() : "");
                    data.put("emailProfesor", prof.getEmail() != null ? prof.getEmail().trim() : "");
                }
            }

            // 🟢 EXTRACCIÓN QUIRÚRGICA DEL TUTOR DE EMPRESA (Índice 0 del Array de MongoDB)
            if (dto.getTutoresEmpresa() != null && !dto.getTutoresEmpresa().isEmpty()) {
                var tutor = dto.getTutoresEmpresa().get(0);
                if (tutor != null) {
                    System.out.println("  -> 👔 Tutor Extraído: " + tutor.getNombre() + " " + tutor.getApellidos());
                    data.put("nombreTutorEmpresa", tutor.getNombre() != null ? tutor.getNombre().trim() : "");
                    data.put("apellidoTutorEmpresa", tutor.getApellidos() != null ? tutor.getApellidos().trim() : "");
                    data.put("dniTutorEmpresa", tutor.getDni() != null ? tutor.getDni().trim() : "");
                    data.put("emailTutorEmpresa", tutor.getEmail() != null ? tutor.getEmail().trim() : "");
                }
            }

            // 🟢 EXTRACCIÓN QUIRÚRGICA DEL ALUMNO ACTIVO (Índice 0 del Array de MongoDB)
            if (dto.getAlumnos() != null && !dto.getAlumnos().isEmpty()) {
                var alu = dto.getAlumnos().get(0);
                if (alu != null) {
                    System.out.println("  -> 👥 Alumno Extraído: " + alu.getNombre() + " " + alu.getApellidos());
                    data.put("nombreAlumno", alu.getNombre() != null ? alu.getNombre().trim() : "");
                    data.put("apellidosAlumno", alu.getApellidos() != null ? alu.getApellidos().trim() : "");
                    data.put("dniAlumno", alu.getDni() != null ? alu.getDni().trim() : "");
                    data.put("fechaNacimientoAlumno", alu.getFechaNacimiento() != null ? alu.getFechaNacimiento().trim() : "");
                    data.put("echaNacimientoAlumo", alu.getFechaNacimiento() != null ? alu.getFechaNacimiento().trim() : ""); // Gazapo Word
                    data.put("codigoCurso", alu.getCodigoCurso() != null ? alu.getCodigoCurso().trim() : "");
                    
                    data.put("fechaInicioAlumno", alu.getFechaInicio() != null ? alu.getFechaInicio().format(FORMATTER) : "");
                    data.put("fechaFinAlumno", alu.getFechaFin() != null ? alu.getFechaFin().format(FORMATTER) : "");
                    
                    data.put("diasSemanaAlumno", alu.getDiasSemana() != null ? alu.getDiasSemana().trim() : "Lunes a Viernes");
                    data.put("horaInicioAlumno", alu.getHoraInicio() != null ? alu.getHoraInicio().trim() : "08:00");
                    data.put("horaFinAlumno", alu.getHoraFin() != null ? alu.getHoraFin().trim() : "14:00");
                    data.put("horasSemanaAlumno", alu.getHorasSemanales() != null ? String.valueOf(alu.getHorasSemanales()) : "30");
                    data.put("totalHorasAlumno", alu.getTotalHorasFormacion() != null ? String.valueOf(alu.getTotalHorasFormacion()) : "400");
                }
            }
        }

        // Impresión de control del mapa definitivo
        System.out.println("  [POI] Mapa 'data' definitivo inyectado de forma aislada para el Anexo 4:");
        data.forEach((k, v) -> System.out.println("    └─ {{ " + k + " }} => " + v));

        System.out.println("  [POI] Ejecutando motor de sustitución seguro...");
        replaceText(document, data);
        System.out.println("========================================================================\n");

        return document;
    }
    /**
     * 💾 ESCRIBIR DOCUMENTO A DISCO
     * Guarda físicamente el archivo procesado en el directorio del servidor de forma atómica
     */
    public void escribirDocumentoADisco(XWPFDocument document, Path destino) throws IOException {
        Files.createDirectories(destino.getParent());
        try (FileOutputStream fos = new FileOutputStream(destino.toFile())) {
            document.write(fos);
        } finally {
            document.close();
        }
    }

    /**
     * 📊 ANEXO 5 DEFINITIVO: Captura el molde, limpia todas las filas en blanco residuales
     * e inserta dinámicamente las filas inicializándolas de forma segura con la API de alto nivel,
     * garantizando un auto-recorte perfecto y respetando fuentes y tamaños del Word.
     */
public void procesarTablaAnexo5(XWPFDocument doc, List<Convenio> convenios, String curso) {
    reemplazarEtiquetasGlobales(doc, curso);

    XWPFTable tablaAnexo = null;
    for (XWPFTable table : doc.getTables()) {
        if (table.getRows().size() >= 2) {
            XWPFTableRow primeraFila = table.getRow(0);
            StringBuilder textoCabecera = new StringBuilder();
            for (XWPFTableCell celda : primeraFila.getTableCells()) {
                textoCabecera.append("[").append(celda.getText().trim()).append("] ");
            }
            String cabeceraStr = textoCabecera.toString().toUpperCase();
            if (cabeceraStr.contains("ACUERDO") || cabeceraStr.contains("EMPRESA") || cabeceraStr.contains("ALUMNO") || cabeceraStr.contains("CALENDARIO")) {
                tablaAnexo = table;
                break;
            }
        }
    }

    if (tablaAnexo == null) return;

    // 1. Capturamos la fila índice 2 (molde de etiquetas) antes de purgar
    XWPFTableRow filaEstiloModelo = tablaAnexo.getRow(2); 
    int totalColumnasModelo = filaEstiloModelo.getTableCells().size();

    // Borramos de golpe toda la zona inferior muerta de filas en blanco de la plantilla
    int totalFilasOriginales = tablaAnexo.getRows().size();
    for (int i = totalFilasOriginales - 1; i >= 3; i--) {
        tablaAnexo.removeRow(i);
    }
    System.out.println("-> 🧼 Tabla purgada. Eliminadas todas las filas en blanco residuales de la plantilla.");

    // 2. Insertamos las filas reales una detrás de otra inmediatamente en el índice 3
    int filaDestinoActual = 3; 
    int numeroCorrelativo = 1;
    java.time.format.DateTimeFormatter formatoEspanol = java.time.format.DateTimeFormatter.ofPattern("dd/MM/yyyy");

    System.out.println("\n🔥 ===== [INSPECCIÓN QUIRÚRGICA DE ESCRITURA DINÁMICA] =====");

    // 🔄 Iteramos la lista de convenios
    for (Convenio conv : convenios) {
        var listaAlumnos = conv.getAlumnos();
        if (listaAlumnos == null || listaAlumnos.isEmpty()) continue;
        
        // 🔄 NUEVO BUCLE: Recorremos CADA alumno del convenio de forma individual
        for (var alumno : listaAlumnos) {

            // Creamos la fila de forma segura usando insertNewTableRow
            XWPFTableRow filaTrabajo = tablaAnexo.insertNewTableRow(filaDestinoActual);
            
            // Heredamos la configuración de altura física de la fila modelo
            if (filaTrabajo.getCtRow() != null && filaEstiloModelo.getCtRow() != null) {
                filaTrabajo.getCtRow().setTrPr(filaEstiloModelo.getCtRow().getTrPr());
            }

            // Inicializamos de forma nativa e individual las celdas en Java 
            for (int cellIdx = 0; cellIdx < totalColumnasModelo; cellIdx++) {
                filaTrabajo.createCell();
            }

            System.out.println("  🎬 -> Escribiendo Alumno en Fila [" + filaDestinoActual + "]: " + alumno.getNombre());

            // Recorremos las celdas de forma blindada
            for (int i = 0; i < totalColumnasModelo; i++) {
                XWPFTableCell celdaModelo = filaEstiloModelo.getCell(i);
                XWPFTableCell celdaNueva = filaTrabajo.getCell(i); 

                // Copiamos el diseño XML de fondo y bordes de la celda modelo a la nueva celda
                if (celdaModelo.getCTTc() != null && celdaNueva.getCTTc() != null && celdaModelo.getCTTc().getTcPr() != null) {
                    celdaNueva.getCTTc().setTcPr((org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr) celdaModelo.getCTTc().getTcPr().copy());
                }

                String textoOriginalEtiqueta = celdaModelo.getText().trim(); 

                // Extraemos tipografía y tamaños originales
                String fuente = "Arial"; 
                int tamano = 11;
                String color = null;
                boolean esNegrita = false;
                boolean esCursiva = false;

                if (!celdaModelo.getParagraphs().isEmpty() && !celdaModelo.getParagraphs().get(0).getRuns().isEmpty()) {
                    XWPFRun runModelo = celdaModelo.getParagraphs().get(0).getRuns().get(0);
                    if (runModelo.getFontFamily() != null) fuente = runModelo.getFontFamily();
                    if (runModelo.getFontSize() != -1) tamano = runModelo.getFontSize();
                    if (runModelo.getColor() != null) color = runModelo.getColor();
                    esNegrita = runModelo.isBold();
                    esCursiva = runModelo.isItalic();
                }

                // Obtención del dato usando la variable 'alumno' en lugar de 'primerAlumno'
                String valorCrudoMongo = null;
                if (i == 0) {
                    valorCrudoMongo = String.format("%02d", numeroCorrelativo);
                } else {
                    if (textoOriginalEtiqueta.contains("{{apellidos}}")) valorCrudoMongo = alumno.getApellidos();
                    else if (textoOriginalEtiqueta.contains("{{nombre}}")) valorCrudoMongo = alumno.getNombre();
                    else if (textoOriginalEtiqueta.contains("{{fechaNacimiento}}")) valorCrudoMongo = alumno.getFechaNacimiento();
                    // 🟢 NUEVAS ETIQUETAS AÑADIDAS AQUÍ:
                    else if (textoOriginalEtiqueta.contains("{{curso}}")) valorCrudoMongo = alumno.getCurso();
                    else if (textoOriginalEtiqueta.contains("{{codigoCurso}}")) valorCrudoMongo = alumno.getCodigoCurso();
                    else if (textoOriginalEtiqueta.contains("{{numeroAcuerdo}}")) valorCrudoMongo = conv.getNumeroAcuerdo();
                    else if (textoOriginalEtiqueta.contains("{{empresaNombre}}")) valorCrudoMongo = conv.getEmpresaNombre();
                    else if (textoOriginalEtiqueta.contains("{{municipio}}")) valorCrudoMongo = conv.getMunicipio();
                    else if (textoOriginalEtiqueta.contains("{{fechaInicio}}")) valorCrudoMongo = alumno.getFechaInicio() != null ? alumno.getFechaInicio().format(formatoEspanol) : null;
                    else if (textoOriginalEtiqueta.contains("{{fechaFin}}")) valorCrudoMongo = alumno.getFechaFin() != null ? alumno.getFechaFin().format(formatoEspanol) : null;
                    else if (textoOriginalEtiqueta.contains("{{totalHoras}}")) {
                        valorCrudoMongo = String.valueOf(alumno.getTotalHorasFormacion()); 
                    }
                    else if (textoOriginalEtiqueta.contains("{{nombreProfesor}}") || textoOriginalEtiqueta.contains("{{apellidosProfesor}}")) {
                        valorCrudoMongo = (conv.getNombreProfesor() + " " + conv.getApellidosProfesor()).trim();
                    }
                }

                String textoFinalAInyectar;
                if (i != 0 && (valorCrudoMongo == null || valorCrudoMongo.isBlank())) {
                    textoFinalAInyectar = textoOriginalEtiqueta;
                } else {
                    textoFinalAInyectar = valorCrudoMongo;
                }

                limpiarContenidoCelda(celdaNueva);
                
                XWPFParagraph parrafo = celdaNueva.getParagraphs().get(0);
                if (!celdaModelo.getParagraphs().isEmpty()) {
                    parrafo.setAlignment(celdaModelo.getParagraphs().get(0).getAlignment());
                }
                
                while (parrafo.getRuns().size() > 0) {
                    parrafo.removeRun(0);
                }
                
                XWPFRun nuevoRun = parrafo.createRun();
                nuevoRun.setText(textoFinalAInyectar != null ? textoFinalAInyectar : "");
                nuevoRun.setFontFamily(fuente);
                if (tamano != -1) nuevoRun.setFontSize(tamano);
                if (color != null) nuevoRun.setColor(color);
                nuevoRun.setBold(esNegrita);
                nuevoRun.setItalic(esCursiva);
            }
            
            // Avanzamos los índices para el SIGUIENTE ALMUNO (dentro del bucle de alumnos)
            filaDestinoActual++;
            numeroCorrelativo++;
        }
    }
    
    // Al final del método (asumiendo que cierras el borrado de la fila modelo aquí)
    if (tablaAnexo.getRows().size() > 2) {
        tablaAnexo.removeRow(2); 
    }
}

    private void replaceText(XWPFDocument doc, Map<String, Object> data) {
        System.out.println("\n🔥 ====== [INICIO DE ESCANEADO Y REEMPLAZO POI] ======");
        
        int parrafosProcesados = 0;
        for (XWPFParagraph paragraph : doc.getParagraphs()) {
            replaceInParagraph(paragraph, data);
            parrafosProcesados++;
        }
        System.out.println("  [POI Debug] Párrafos sueltos analizados fuera de tablas: " + parrafosProcesados);

        int celdasProcesadas = 0;
        for (XWPFTable table : doc.getTables()) {
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    for (XWPFParagraph paragraph : cell.getParagraphs()) {
                        replaceInParagraph(paragraph, data);
                        celdasProcesadas++;
                    }
                }
            }
        }
        System.out.println("  [POI Debug] Párrafos analizados dentro de celdas de tablas: " + celdasProcesadas);
        System.out.println("🔥 ====== [FIN DE ESCANEADO Y REEMPLAZO POI] ======\n");
    }

    /**
     * Aplica el reemplazo de etiquetas unificando los runs del párrafo pero 
     * CONSERVANDO intacto el tipo de letra, tamaño y estilos originales del documento.
     */
    private void replaceInParagraph(XWPFParagraph paragraph, Map<String, Object> data) {
        String paragraphText = paragraph.getParagraphText();
        // Control preventivo rápido: si no contiene el inicio de llaves, saltamos el párrafo
        if (paragraphText == null || !paragraphText.contains("{{")) return;

        System.out.println("    🔎 [PÁRRAFO LEÍDO] Texto original en plantilla: \"" + paragraphText.trim() + "\"");

        boolean cambiado = false;
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            String key = "{{" + entry.getKey() + "}}";
            
            if (paragraphText.contains(key)) {
                String val = entry.getValue() == null ? "" : entry.getValue().toString();
                System.out.println("      🎯 ¡COINCIDENCIA! Reemplazando " + key + " por '" + val + "'");
                
                paragraphText = paragraphText.replace(key, val);
                cambiado = true;
            }
        }

        // 🟢 PROCESAMIENTO SEGURO DE REESCRITURA XML SIN PÉRDIDA DE TEXTO ADYACENTE
        if (cambiado) {
            System.out.println("      ✍️ [FIX XML] Escribiendo texto unificado en el Word: \"" + paragraphText.trim() + "\"");
            
            List<XWPFRun> runs = paragraph.getRuns();
            if (runs != null && !runs.isEmpty()) {
                
                // 1. Localizamos el primer run comprometido con llaves o texto para heredar estilos
                XWPFRun runModelo = runs.get(0);
                
                String fuente = runModelo.getFontFamily();
                int tamano = runModelo.getFontSize();
                String color = runModelo.getColor();
                boolean esNegrita = runModelo.isBold();
                boolean esCursiva = runModelo.isItalic();
                boolean esSubrayado = runModelo.getUnderline() != UnderlinePatterns.NONE;

                // 2. ⚡ LA CORRECCIÓN: En vez de hacer removeRun destructivo, volcamos todo el texto 
                // ya procesado en el run 0. Esto frena en seco las pérdidas de texto adyacente.
                runModelo.setText(paragraphText, 0);

                // Re-aplicamos tus estilos corporativos originales al run principal
                if (fuente != null) runModelo.setFontFamily(fuente);
                if (tamano != -1) runModelo.setFontSize(tamano);
                if (color != null) runModelo.setColor(color);
                runModelo.setBold(esNegrita);
                runModelo.setItalic(esCursiva);
                if (esSubrayado) runModelo.setUnderline(UnderlinePatterns.SINGLE);

                // 3. Purgamos de forma atómica únicamente los trozos de runs XML fragmentados 
                // huérfanos que dejó Microsoft Word ("nombreAlumno", "}}") para que no aparezcan duplicados
                for (int i = runs.size() - 1; i > 0; i--) {
                    paragraph.removeRun(i);
                }
            } else {
                // Fallback si por alguna razón el párrafo venía sin runs iniciales
                XWPFRun nuevoRun = paragraph.createRun();
                nuevoRun.setText(paragraphText);
                nuevoRun.setFontFamily("Arial");
                nuevoRun.setFontSize(10);
            }
        }
    }

    /**
     * Intenta extraer un valor String de varios getters posibles del objeto usando reflexión.
     */
    private String tryExtractString(Object source, String... getters) {
        if (source == null || getters == null) {
            return "";
        }
        for (String getter : getters) {
            if (getter == null || getter.isBlank()) {
                continue;
            }
            try {
                java.lang.reflect.Method method = source.getClass().getMethod(getter);
                Object value = method.invoke(source);
                if (value != null) {
                    return value.toString();
                }
            } catch (Exception e) {
                // Ignoramos métodos no existentes o fallos de invocación y seguimos
            }
        }
        return "";
    }
      private List extractResultadosAprendizaje(Convenio dto) {
        // 1. Control de nulos sobre el convenio y su lista de alumnos
        if (dto == null || dto.getAlumnos() == null || dto.getAlumnos().isEmpty()) {
            return java.util.Collections.emptyList();
        }

        // 2. Extraemos el primer alumno vinculado
        var primerAlumno = dto.getAlumnos().get(0);
        if (primerAlumno == null || primerAlumno.getResultadosAprendizaje() == null) {
            return java.util.Collections.emptyList();
        }

        // 3. ⚡ Retornamos el saco de datos pedagógicos guardado en caliente desde el frontal
        return primerAlumno.getResultadosAprendizaje();
    }

    /**
     * Auxiliar: Reemplaza variables genéricas en los encabezados fijos únicamente si contienen datos válidos.
     */
    private void reemplazarEtiquetasGlobales(XWPFDocument doc, String curso) {
        String cursoAcademico = (curso != null && !curso.isBlank()) ? curso : null;       
        // 🚀 Generamos la fecha de hoy. Si por requisitos del centro deseas que bajo ciertas 
        // condiciones no se sustituya, puedes forzar un null aquí para congelar la etiqueta.
        String fechaHoyFormateada = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("dd/MM/yyyy"));

        for (XWPFParagraph p : doc.getParagraphs()) {
            String text = p.getParagraphText();
            if (text != null) {
                boolean cambiado = false;
                
                // Solo sustituye si el curso tiene un valor real transferido del Frontend
                if (cursoAcademico != null && text.contains("{{cursoAcademico}}")) {
                    text = text.replace("{{cursoAcademico}}", cursoAcademico);
                    cambiado = true;
                }
                
                // Solo sustituye si se desea inyectar la fecha actual del sistema
                if (fechaHoyFormateada != null && text.contains("{{fechaHoy}}")) {
                    text = text.replace("{{fechaHoy}}", fechaHoyFormateada);
                    cambiado = true;
                }

                // Si no se cumplieron las condiciones, no se invoca removeRun() y se conserva la etiqueta muerta original
                if (cambiado) {
                    for (int i = p.getRuns().size() - 1; i >= 0; i--) { p.removeRun(i); }
                    XWPFRun newRun = p.createRun();
                    newRun.setText(text);
                }
            }
        }
    }

    /**
     * 🧼 Limpia de forma profunda y agresiva todo el contenido, párrafos y texto residual 
     * de una celda reutilizada, asegurando que no queden datos en la caché de Apache POI.
     */
    private void limpiarContenidoCelda(XWPFTableCell celda) {
        if (celda == null) return;

        // 1. Eliminamos todos los párrafos extras acumulados en la celda de arriba hacia abajo
        // dejando únicamente un esqueleto de párrafo virgen en la posición 0
        while (celda.getParagraphs().size() > 1) {
            celda.removeParagraph(1);
        }

        // 2. Accedemos al único párrafo superviviente de la celda
        XWPFParagraph parrafoBase = celda.getParagraphs().get(0);
        if (parrafoBase != null) {
            // 3. Borramos todos los 'runs' (fragmentos de texto) que tuviera el párrafo de vueltas anteriores
            for (int i = parrafoBase.getRuns().size() - 1; i >= 0; i--) {
                parrafoBase.removeRun(i);
            }
            
            // 4. Forzamos la limpieza del texto plano subyacente en el XML nativo de Word
            if (parrafoBase.getCTP() != null) {
                // Esto elimina cualquier nodo <w:r> residual directamente del XML de la palabra
                while (parrafoBase.getCTP().sizeOfRArray() > 0) {
                    parrafoBase.getCTP().removeR(0);
                }
            }
        }
    }
      /**
     * 📄 ANEXO 6: Rellena la tabla del Plan de Formación agrupando los RAs 
     * que pertenecen a la familia profesional del convenio actual.
     */
    // 🔄 CORRECCIÓN FIRMA: Ahora recibe la entidad unificada Convenio directamente
    public void procesarTablaAnexo6(XWPFDocument doc, Convenio dto) {
          if (doc == null || doc.getTables().isEmpty()) return;
        
        XWPFTable tablaPlan = doc.getTables().get(0); // Buscamos la tabla del Plan de Formación
        if (tablaPlan.getRows().size() < 3) return; // Validación preventiva para evitar IndexOutOfBounds
        
        XWPFTableRow filaModelo = tablaPlan.getRow(2); // Fila molde de RAs

        // 1. 👥 Extraemos de forma segura el alumno principal del convenio
        var listaAlumnos = (dto != null) ? dto.getAlumnos() : null;
        var primerAlumno = (listaAlumnos != null && !listaAlumnos.isEmpty()) ? listaAlumnos.get(0) : null;

        // 2. 🟢 EXTRACCIÓN SEGURO-DIRECTA: Leemos los RAs incrustados en la ficha del alumno
        var listaRAs = (primerAlumno != null) ? primerAlumno.getResultadosAprendizaje() : null;
        
        if (listaRAs == null || listaRAs.isEmpty()) {
            System.out.println("⚠️ [POI] Anexo 6 cancelado: El alumno del convenio no contiene Resultados de Aprendizaje.");
            return;
        }

        int indexFila = 2;
        for (var ra : listaRAs) {
            // Evitamos errores de lógica si el RA viene con objetos internos vacíos
            if (ra == null) continue;
            
            XWPFTableRow nuevaFila = tablaPlan.insertNewTableRow(indexFila);
            
            // Inicializamos celdas e inyectamos los datos por columna respetando el total del modelo
            int totalCeldasModelo = filaModelo.getTableCells().size();
            for (int i = 0; i < totalCeldasModelo; i++) {
                XWPFTableCell celdaNueva = nuevaFila.createCell();
                limpiarContenidoCelda(celdaNueva);
                
                XWPFRun run = celdaNueva.getParagraphs().get(0).createRun();
                
                // Formateamos las fuentes por defecto para que no use la letra de fallback de Word
                run.setFontFamily("Arial");
                run.setFontSize(10);
                
                switch (i) {
                    case 0 -> {
                        if (ra.getModulo() != null && ra.getModulo() != null) {
                            run.setText(ra.getModulo()); // Columna Módulo
                        }
                    }
                    case 1 -> {
                        if (ra.getReducido() != null) run.setText(ra.getReducido()); // Columna Código RA
                    }
                    case 2 -> {
                        if (ra.getNombreDescripcion() != null) run.setText(ra.getNombreDescripcion()); // Columna Descripción Competencia
                    }
                }
            }
            indexFila++;
        }
        
        // Borramos de forma segura el molde residual de la plantilla
        if (tablaPlan.getRows().size() > indexFila) {
            tablaPlan.removeRow(indexFila); 
            System.out.println("-> 🧼 Purgado con éxito el molde de etiquetas del Anexo 6.");
        }
    }
     private final Anexo6Processor anexo6Processor;
    public XWPFDocument procesarDocumentoEnMemoriaAnexo6(GenerarDocumentoContext ctx) throws IOException {
        ClassPathResource resource = new ClassPathResource("plantillas/" + ctx.plantilla());
        InputStream fis = resource.getInputStream();
        XWPFDocument document = new XWPFDocument(fis);

        Convenio dto = ctx.datos();
        if (dto != null) {
            dto.sincronizarCamposPlanosTemplate();
        }

        // 1. 📂 Inicializamos el mapa de datos base
        Map<String, Object> data = new HashMap<>();
        
        if (dto != null) {
            data.put("numeroAcuerdo", dto.getNumeroAcuerdo() != null ? dto.getNumeroAcuerdo() : "");
            data.put("cursoAcademico", dto.getCursoAcademico() != null ? dto.getCursoAcademico() : "");
            data.put("nombreEmpresa", dto.getEmpresaNombre() != null ? dto.getEmpresaNombre() : "");

            // 🎯 CAPTURA DINÁMICA DEL ALUMNO PARA LAS CABECERAS
            var listaAlumnos = dto.getAlumnos();
            var primerAlumno = (listaAlumnos != null && !listaAlumnos.isEmpty()) ? listaAlumnos.get(0) : null;
            if (primerAlumno != null) {
                data.put("nombreAlumno", primerAlumno.getNombre() != null ? primerAlumno.getNombre() : "");
                data.put("apellidosAlumno", primerAlumno.getApellidos() != null ? primerAlumno.getApellidos() : "");
                data.put("dniAlumno", primerAlumno.getDni() != null ? primerAlumno.getDni() : "");
                data.put("cicloFormativo", primerAlumno.getCicloFormativo() != null ? primerAlumno.getCicloFormativo() : "");
            }

            // ⚡ 🔥 LA CLAVE MAESTRA: Llamamos al procesador que creamos
            if (anexo6Processor != null) {
                anexo6Processor.procesarDatosEspecificos(ctx, data);
                System.out.println("🟢 [STRATEGY] Anexo6Processor ha inyectado con éxito todos los campos de contacto en el mapa 'data'.");
            } else {
                System.out.println("⚠️ [ALERTA] El anexo6Processor no está correctamente inyectado.");
            }
        }
        data.put("fechaHoy", LocalDate.now().format(FORMATTER));

        // 2. 📝 REEMPLAZO EN PÁRRAFOS SUELTOS (Fuera de tablas)
        for (XWPFParagraph paragraph : document.getParagraphs()) {
            replaceInParagraph(paragraph, data);
        }

        // =========================================================================
        // 3. 🔄 REEMPLAZO EN FRASES CON RECURSIVIDAD PARA TABLAS ANIDADAS
        // =========================================================================
        int totalTablasExistentes = document.getTables().size();
        System.out.println("  [POI] Escaneando " + totalTablasExistentes + " tablas raíz en busca de etiquetas de fechas/horarios...");

        for (int t = 0; t < totalTablasExistentes; t++) {
            XWPFTable tablaAExaminar = document.getTables().get(t);
            
            // Si la celda contiene {{modulo}} o {{reducido}}, saltamos la tabla porque es la dinámica de abajo
            if (tablaAExaminar.getRows().size() > 0) {
                String textoFilaMuestra = obtenerTextoDeFila(tablaAExaminar.getRow(0));
                if (textoFilaMuestra.contains("{{modulo}}") || textoFilaMuestra.contains("DENOMINACIÓN")) {
                    System.out.println("  -> [POI] Tabla índice [" + t + "] identificada como tabla de RAs. Saltando reemplazo plano.");
                    continue; 
                }
            }

            // ⚡ LLAMADA RECURSIVA: Procesa la tabla principal y destripa las subtablas de periodos/horarios
            procesarTablaYSubtablasRecursivo(tablaAExaminar, data);
        }
        System.out.println("  [POI] Sustitución de etiquetas fijas en tablas y cabeceras completada con éxito.");

        // 3. BÚSQUEDA POR TEXTO REAL DE CABECERA (ANEXO 6)
        XWPFTable tablaPlan = null;
        int indiceFilaModelo = 2; 
        int totalTablas = document.getTables().size();
        
        System.out.println("  [POI] Iniciando rastreo de cabeceras en las " + totalTablas + " tablas...");

        for (int t = 0; t < totalTablas; t++) {
            XWPFTable table = document.getTables().get(t);
            int maxFilasAExaminar = Math.min(table.getRows().size(), 4);
            
            for (int r = 0; r < maxFilasAExaminar; r++) {
                String textoFila = obtenerTextoDeFila(table.getRow(r));
                if (textoFila.contains("DENOMINACIÓN") && textoFila.contains("RESULTADOS") && textoFila.contains("APRENDIZAJE")) {
                    tablaPlan = table;
                    indiceFilaModelo = r + 1; 
                    System.out.println("  🎯 ¡TABLA IDENTIFICADA CON ÉXITO! Localizada en la Tabla índice [" + t + "]");
                    break;
                }
            }
            if (tablaPlan != null) break; 
        }

        if (tablaPlan == null && totalTablas > 0) {
            tablaPlan = document.getTables().get(totalTablas - 1);
            indiceFilaModelo = (tablaPlan.getRows().size() >= 3) ? 2 : 1;
        }

        if (tablaPlan == null) {
            System.out.println("  ❌ ERROR CRÍTICO: El documento no contiene ninguna tabla.");
            return document;
        }

        // =========================================================================
        // 🚀 PROCESAMIENTO DINÁMICO COMPLETADO E INDENTADO CORRECTAMENTE
        // =========================================================================
        var listaAlumnos = dto != null ? dto.getAlumnos() : null;
        var primerAlumno = (listaAlumnos != null && !listaAlumnos.isEmpty()) ? listaAlumnos.get(0) : null;
        var listaRAs = (primerAlumno != null) ? primerAlumno.getResultadosAprendizaje() : null;

        if (tablaPlan.getRows().size() > indiceFilaModelo && listaRAs != null && !listaRAs.isEmpty()) {
            XWPFTableRow filaEstiloModelo = tablaPlan.getRow(indiceFilaModelo);
            int totalColumnasModelo = filaEstiloModelo.getTableCells().size();
            int filaDestinoActual = indiceFilaModelo + 1; 

            System.out.println("  🔥 -> [POI] Escribiendo " + listaRAs.size() + " RAs pedagógicos desde la ficha del Alumno.");

            for (var ra : listaRAs) {
                if (ra == null) continue;

                XWPFTableRow filaTrabajo = tablaPlan.insertNewTableRow(filaDestinoActual);
                
                if (filaTrabajo.getCtRow() != null && filaEstiloModelo.getCtRow() != null) {
                    filaTrabajo.getCtRow().setTrPr(filaEstiloModelo.getCtRow().getTrPr());
                }

                for (int cellIdx = 0; cellIdx < totalColumnasModelo; cellIdx++) {
                    XWPFTableCell celdaNueva = filaTrabajo.createCell();
                    XWPFTableCell celdaModelo = filaEstiloModelo.getCell(cellIdx);
                    
                    if (celdaModelo != null && celdaModelo.getCTTc() != null && celdaNueva.getCTTc() != null && celdaModelo.getCTTc().getTcPr() != null) {
                        celdaNueva.getCTTc().setTcPr((org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr) celdaModelo.getCTTc().getTcPr().copy());
                    }
                    
                    limpiarContenidoCelda(celdaNueva);
                    XWPFParagraph parrafo = celdaNueva.getParagraphs().get(0);
                    
                    if (celdaModelo != null && !celdaModelo.getParagraphs().isEmpty()) {
                        parrafo.setAlignment(celdaModelo.getParagraphs().get(0).getAlignment());
                    }

                    XWPFRun run = parrafo.createRun();
                    run.setFontFamily("Arial");
                    run.setFontSize(10);

                    String textoOriginalEtiqueta = (celdaModelo != null) ? celdaModelo.getText().trim() : "";
                    String valorFinalAInyectar = "";

                    if (textoOriginalEtiqueta.contains("{{modulo}}") || textoOriginalEtiqueta.contains("MODULO")) {
                        valorFinalAInyectar = ra.getModulo() != null ? ra.getModulo() : "";
                        run.setBold(true); 
                    } else if (textoOriginalEtiqueta.contains("{{reducido}}") || textoOriginalEtiqueta.contains("CODIGORA")) {
                        valorFinalAInyectar = ra.getReducido() != null ? ra.getReducido() : "";
                        run.setBold(true);
                    } else if (textoOriginalEtiqueta.contains("{{nombreDescripcion}}") || textoOriginalEtiqueta.contains("DESCRIPCION")) {
                        valorFinalAInyectar = ra.getNombreDescripcion() != null ? ra.getNombreDescripcion() : "";
                    } else {
                        valorFinalAInyectar = textoOriginalEtiqueta;
                    }
                    run.setText(valorFinalAInyectar);
                }
                filaDestinoActual++;
            }
            tablaPlan.removeRow(indiceFilaModelo);
        }
        return document;
    }

    /**
     * 🕵️ MÉTODO AUXILIAR RECURSIVO: Escanea y destripa subtablas anidadas (Períodos, Calendario y Horario)
     */
    private void procesarTablaYSubtablasRecursivo(XWPFTable tabla, Map<String, Object> data) {
        for (XWPFTableRow row : tabla.getRows()) {
            for (XWPFTableCell cell : row.getTableCells()) {
                
                // A) Reemplazamos en los párrafos normales de la celda actual
                for (XWPFParagraph pCelda : cell.getParagraphs()) {
                    replaceInParagraph(pCelda, data); 
                }
                
                // B) 🟢 DETECTOR ANIDADO: Si la celda esconde mini-tablas dentro, bajamos de forma recursiva a resolverlas
                if (cell.getTables() != null && !cell.getTables().isEmpty()) {
                    for (XWPFTable subTabla : cell.getTables()) {
                        System.out.println("📦 [POI RECURSIVO] Detectada doble tabla (Periodo/Calendario/Horario). Destripando sub-celdas...");
                        procesarTablaYSubtablasRecursivo(subTabla, data);
                         // 🔥 Llamada recursiva
                    }
                }
            }
        }
    }
/**
 * 🛠️ Auxiliar: Extrae de forma segura todo el texto plano de las celdas de una fila
 */
private String obtenerTextoDeFila(XWPFTableRow fila) {
    if (fila == null) return "";
    StringBuilder resultado = new StringBuilder();
    for (XWPFTableCell celda : fila.getTableCells()) {
        if (celda != null && celda.getText() != null) {
            resultado.append(celda.getText()).append(" ");
        }
    }
    return resultado.toString().toUpperCase().trim();
}
     /**
     * 📊 ANEXO 8 MEMORIA: Procesa la Ficha de Seguimiento Periódico.
     * Escanea y localiza la tabla de evaluación e inyecta los RAs mapeando las 7 columnas reales.
     */
        // 🟢 RECUERDA: Declara arriba en la cabecera de la clase: private final Anexo8Processor anexo8Processor;
     private final Anexo8Processor anexo8Processor;
         public XWPFDocument procesarDocumentoEnMemoriaAnexo8(GenerarDocumentoContext ctx) throws IOException {
        ClassPathResource resource = new ClassPathResource("plantillas/" + ctx.plantilla());
        InputStream fis = resource.getInputStream();
        XWPFDocument document = new XWPFDocument(fis);

        Convenio dto = ctx.datos();
        if (dto != null) {
            dto.sincronizarCamposPlanosTemplate();
        }

        Map<String, Object> data = new HashMap<>();
        if (dto != null) {
            data.put("numeroAcuerdo", dto.getNumeroAcuerdo() != null ? dto.getNumeroAcuerdo() : "");
            data.put("cursoAcademico", dto.getCursoAcademico() != null ? dto.getCursoAcademico() : "");
            data.put("nombreEmpresa", dto.getEmpresaNombre() != null ? dto.getEmpresaNombre() : "");
            data.put("nombreProfesor", (dto.getNombreProfesor() + " " + dto.getApellidosProfesor()).trim());

            var listaAlumnos = dto.getAlumnos();
            var primerAlumno = (listaAlumnos != null && !listaAlumnos.isEmpty()) ? listaAlumnos.get(0) : null;
            if (primerAlumno != null) {
                data.put("nombreAlumno", primerAlumno.getNombre() != null ? primerAlumno.getNombre() : "");
                data.put("apellidosAlumno", primerAlumno.getApellidos() != null ? primerAlumno.getApellidos() : "");
                data.put("dniAlumno", primerAlumno.getDni() != null ? primerAlumno.getDni() : "");
                data.put("cicloFormativo", primerAlumno.getCicloFormativo() != null ? primerAlumno.getCicloFormativo() : "");
            }

            if (anexo8Processor != null) {
                anexo8Processor.procesarDatosEspecificos(ctx, data);
            }
        }
        data.put("fechaHoy", LocalDate.now().format(FORMATTER));

        // 1. Reemplazo en párrafos libres del documento (cabeceras fuera de tablas)
        for (XWPFParagraph paragraph : document.getParagraphs()) {
            replaceInParagraph(paragraph, data);
        }

        // =========================================================================
        // ⚡ DETECCIÓN CELDA POR CELDA EN LA TABLA HÍBRIDA (ANEXO 8)
        // =========================================================================
        XWPFTable tablaHibrida = null;
        int indiceFilaModelo = -1;

        for (XWPFTable tabla : document.getTables()) {
            for (int r = 0; r < tabla.getRows().size(); r++) {
                XWPFTableRow fila = tabla.getRow(r);
                
                boolean esFilaMoldeRA = false;
                for (XWPFTableCell celdaEval : fila.getTableCells()) {
                    String textoCelda = celdaEval.getText().trim();
                    if (textoCelda.contains("{{modulo}}") || textoCelda.contains("{{reducido}}") || 
                        textoCelda.contains("CODIGORA") || textoCelda.contains("{{CODIGORA}}")) {
                        esFilaMoldeRA = true;
                        break; 
                    }
                }

                if (esFilaMoldeRA) {
                    tablaHibrida = tabla;
                    indiceFilaModelo = r;
                    System.out.println("🎯 [POI] Fila molde de RAs detectada celda a celda en la Fila [" + r + "]");
                    continue; 
                }

                for (XWPFTableCell celda : fila.getTableCells()) {
                    for (XWPFParagraph p : celda.getParagraphs()) {
                        replaceInParagraph(p, data);
                    }
                    if (celda.getTables() != null) {
                        for (XWPFTable subTabla : celda.getTables()) {
                            for (XWPFTableRow subRow : subTabla.getRows()) {
                                for (XWPFTableCell subCell : subRow.getTableCells()) {
                                    for (XWPFParagraph pSub : subCell.getParagraphs()) {
                                        replaceInParagraph(pSub, data);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        if (tablaHibrida == null || indiceFilaModelo == -1) {
            System.out.println("❌ ERROR CRÍTICO: No se localizó la estructura de RAs celda a celda en la plantilla del Anexo 8.");
            return document;
        }

        // =========================================================================
        // 🚀 INYECCIÓN DINÁMICA CLONADA CON PROTECCIÓN DE MEMORIA Y CHECKBOXES
        // =========================================================================
        var listaAlumnos = dto != null ? dto.getAlumnos() : null;
        var primerAlumno = (listaAlumnos != null && !listaAlumnos.isEmpty()) ? listaAlumnos.get(0) : null;
        var listaRA = (primerAlumno != null) ? primerAlumno.getResultadosAprendizaje() : null;

        if (listaRA != null && !listaRA.isEmpty()) {
            XWPFTableRow filaEstiloModelo = tablaHibrida.getRow(indiceFilaModelo);
            int totalColumnasModelo = filaEstiloModelo.getTableCells().size();
            int filaDestinoActual = indiceFilaModelo + 1;

            // 🟢 PROTECCIÓN DE MEMORIA: Almacenamos los tokens del molde original en un array cerrado
            List<String> etiquetasOriginalesMemoria = new ArrayList<>();
            for (int cellIdx = 0; cellIdx < totalColumnasModelo; cellIdx++) {
                XWPFTableCell celdaM = filaEstiloModelo.getCell(cellIdx);
                etiquetasOriginalesMemoria.add(celdaM != null ? celdaM.getText().trim() : "");
            }

            System.out.println("🔥 [POI] Estirando " + listaRA.size() + " RAs secuenciales (RA-1 al RA-8) con checkboxes Unicode manuales.");

            for (int indexRA = 0; indexRA < listaRA.size(); indexRA++) {
                var ra = listaRA.get(indexRA);
                if (ra == null) continue;

                XWPFTableRow filaTrabajo;
                if (indexRA == 0) {
                    filaTrabajo = filaEstiloModelo; 
                } else {
                    filaTrabajo = tablaHibrida.insertNewTableRow(filaDestinoActual);
                    if (filaTrabajo.getCtRow() != null && filaEstiloModelo.getCtRow() != null) {
                        filaTrabajo.getCtRow().setTrPr(filaEstiloModelo.getCtRow().getTrPr());
                    }
                    
                    for (int cellIdx = 0; cellIdx < totalColumnasModelo; cellIdx++) {
                        XWPFTableCell celdaOriginal = filaEstiloModelo.getCell(cellIdx);
                        XWPFTableCell celdaNueva = filaTrabajo.createCell();
                        if (celdaOriginal != null && celdaOriginal.getCTTc() != null && celdaNueva.getCTTc() != null && celdaOriginal.getCTTc().getTcPr() != null) {
                            celdaNueva.getCTTc().setTcPr((org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr) celdaOriginal.getCTTc().getTcPr().copy());
                        }
                    }
                }

                String codigoRaSeguro = ra.getReducido() != null ? ra.getReducido() : "";
                String moduloSeguro = ra.getModulo() != null ? ra.getModulo() : "";

                // Escritura atómica por columna
                for (int i = 0; i < totalColumnasModelo; i++) {
                    XWPFTableCell celdaNueva = filaTrabajo.getCell(i);
                    String textoOriginalEtiqueta = etiquetasOriginalesMemoria.get(i);

                    // 1. Columna del Módulo
                    if (textoOriginalEtiqueta.contains("{{modulo}}") || textoOriginalEtiqueta.contains("MODULO")) {
                        limpiarContenidoCelda(celdaNueva);
                        XWPFParagraph parrafo = celdaNueva.getParagraphs().get(0);
                        XWPFRun run = parrafo.createRun();
                        run.setFontFamily("Arial"); run.setFontSize(10);
                        run.setText(moduloSeguro);
                    } 
                    // 2. Columna del Código Reducido (RA-1, RA-2...)
                    else if (textoOriginalEtiqueta.contains("{{reducido}}") || textoOriginalEtiqueta.contains("CODIGORA") || textoOriginalEtiqueta.contains("{{CODIGORA}}")) {
                        limpiarContenidoCelda(celdaNueva);
                        XWPFParagraph parrafo = celdaNueva.getParagraphs().get(0);
                        XWPFRun run = parrafo.createRun();
                        run.setFontFamily("Arial"); run.setFontSize(10); run.setBold(true);
                        run.setText(codigoRaSeguro);
                    } 
                    // 3. 🟢 NUEVO: Columnas de Evaluación (Superado, No superado, En proceso)
                    // Si el diseño de tu tabla tiene más columnas fijas (como Observaciones al final),
                    // aseguramos que pinte el cuadradito solo en las columnas intermedias de checkboxes
                    else if (i >= 2 && i <= 5) { 
                        limpiarContenidoCelda(celdaNueva);
                        XWPFParagraph parrafo = celdaNueva.getParagraphs().get(0);
                        parrafo.setAlignment(ParagraphAlignment.CENTER); // 🎯 Centramos el cuadradito en la celda
                        
                        XWPFRun run = parrafo.createRun();
                        run.setFontFamily("MS Gothic"); // 💡 Tipografía recomendada para que el carácter unicode no se deforme en Word/PDF
                        run.setFontSize(12); // Un pelín más grande para que luzca como un checkbox real
                        run.setText("\u2610"); // 🔲 Inyectamos el carácter Unicode del recuadro vacío de forma nativa
                    }
                    // 4. Columna de Observaciones o cualquier otra columna extra del final
                    else {
                        if (indexRA == 0) {
                            for (XWPFParagraph pCelda : celdaNueva.getParagraphs()) {
                                replaceInParagraph(pCelda, data);
                            }
                        }
                    }
                }
                filaDestinoActual++;
            }
            System.out.println("-> [POI] Tabla híbrida del Anexo 8 consolidada con éxito.");
        }

        return document;
    }

    /**
     * 📊 ANEXO 9 MEMORIA: Procesa la Ficha de Valoración Final.
     * Escanea y localiza la tabla de evaluación e inyecta los RAs mapeando las 4 columnas reales.
     */
    private final Anexo9Processor anexo9Processor;
    public XWPFDocument procesarDocumentoEnMemoriaAnexo9(GenerarDocumentoContext ctx) throws IOException {
        ClassPathResource resource = new ClassPathResource("plantillas/" + ctx.plantilla());
        InputStream fis = resource.getInputStream();
        XWPFDocument document = new XWPFDocument(fis);

        Convenio dto = ctx.datos();
        if (dto != null) {
            dto.sincronizarCamposPlanosTemplate();
        }

        Map<String, Object> data = new HashMap<>();
        if (dto != null) {
            data.put("numeroAcuerdo", dto.getNumeroAcuerdo() != null ? dto.getNumeroAcuerdo() : "");
            data.put("cursoAcademico", dto.getCursoAcademico() != null ? dto.getCursoAcademico() : "");
            data.put("nombreEmpresa", dto.getEmpresaNombre() != null ? dto.getEmpresaNombre() : "");
            data.put("nombreProfesor", (dto.getNombreProfesor() + " " + dto.getApellidosProfesor()).trim());

            var listaAlumnos = dto.getAlumnos();
            var primerAlumno = (listaAlumnos != null && !listaAlumnos.isEmpty()) ? listaAlumnos.get(0) : null;
            if (primerAlumno != null) {
                data.put("nombreAlumno", primerAlumno.getNombre() != null ? primerAlumno.getNombre() : "");
                data.put("apellidosAlumno", primerAlumno.getApellidos() != null ? primerAlumno.getApellidos() : "");
                data.put("dniAlumno", primerAlumno.getDni() != null ? primerAlumno.getDni() : "");
                data.put("cicloFormativo", primerAlumno.getCicloFormativo() != null ? primerAlumno.getCicloFormativo() : "");
            }

            // 🟢 Inyectamos tu procesador específico del Anexo 9
            if (anexo9Processor != null) {
                anexo9Processor.procesarDatosEspecificos(ctx, data);
            }
        }
        data.put("fechaHoy", LocalDate.now().format(FORMATTER));

        // 1. Reemplazo en párrafos libres del documento (cabeceras fuera de tablas)
        for (XWPFParagraph paragraph : document.getParagraphs()) {
            replaceInParagraph(paragraph, data);
        }

        // =========================================================================
        // ⚡ DETECCIÓN CELDA POR CELDA EN LA TABLA HÍBRIDA (ANEXO 9)
        // =========================================================================
        XWPFTable tablaValoracion = null;
        int indiceFilaModelo = -1;

        for (XWPFTable tabla : document.getTables()) {
            for (int r = 0; r < tabla.getRows().size(); r++) {
                XWPFTableRow fila = tabla.getRow(r);
                
                boolean esFilaMoldeRA = false;
                for (XWPFTableCell celdaEval : fila.getTableCells()) {
                    String textoCelda = celdaEval.getText().trim();
                    if (textoCelda.contains("{{modulo}}") || textoCelda.contains("{{reducido}}") || 
                        textoCelda.contains("CODIGORA") || textoCelda.contains("MÓDULO") || textoCelda.contains("RESULTADO")) {
                        esFilaMoldeRA = true;
                        break; 
                    }
                }

                if (esFilaMoldeRA) {
                    tablaValoracion = tabla;
                    indiceFilaModelo = r;
                    System.out.println("🎯 [POI] Fila molde de RAs detectada celda a celda en la Fila [" + r + "] para el Anexo 9");
                    continue; 
                }

                for (XWPFTableCell celda : fila.getTableCells()) {
                    for (XWPFParagraph p : celda.getParagraphs()) {
                        replaceInParagraph(p, data);
                    }
                    if (celda.getTables() != null) {
                        for (XWPFTable subTabla : celda.getTables()) {
                            for (XWPFTableRow subRow : subTabla.getRows()) {
                                for (XWPFTableCell subCell : subRow.getTableCells()) {
                                    for (XWPFParagraph pSub : subCell.getParagraphs()) {
                                        replaceInParagraph(pSub, data);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        if (tablaValoracion == null || indiceFilaModelo == -1) {
            System.out.println("❌ ERROR CRÍTICO: No se localizó la estructura de RAs celda a celda en la plantilla del Anexo 9.");
            return document;
        }

        // =========================================================================
        // 🚀 INYECCIÓN FIEL SOBRE LAS 4 COLUMNAS CON PROTECCIÓN DE MEMORIA Y CHECKBOXES UNICODE
        // =========================================================================
        var listaAlumnos = dto != null ? dto.getAlumnos() : null;
        var primerAlumno = (listaAlumnos != null && !listaAlumnos.isEmpty()) ? listaAlumnos.get(0) : null;
        var listaRA = (primerAlumno != null) ? primerAlumno.getResultadosAprendizaje() : null;

        if (listaRA != null && !listaRA.isEmpty()) {
            XWPFTableRow filaEstiloModelo = tablaValoracion.getRow(indiceFilaModelo);
            int totalColumnasModelo = filaEstiloModelo.getTableCells().size();
            int filaDestinoActual = indiceFilaModelo + 1;

            // 🟢 PROTECCIÓN DE MEMORIA: Almacenamos los tokens del molde original en un array cerrado
            List<String> etiquetasOriginalesMemoria = new ArrayList<>();
            for (int cellIdx = 0; cellIdx < totalColumnasModelo; cellIdx++) {
                XWPFTableCell celdaM = filaEstiloModelo.getCell(cellIdx);
                etiquetasOriginalesMemoria.add(celdaM != null ? celdaM.getText().trim() : "");
            }

            System.out.println("🔥 [POI] Estirando " + listaRA.size() + " RAs secuenciales (RA-1 al RA-8) en la matriz del Anexo 9.");

            for (int indexRA = 0; indexRA < listaRA.size(); indexRA++) {
                var ra = listaRA.get(indexRA);
                if (ra == null) continue;

                XWPFTableRow filaTrabajo;
                if (indexRA == 0) {
                    filaTrabajo = filaEstiloModelo; // Reutilizamos el molde en la primera línea física
                } else {
                    filaTrabajo = tablaValoracion.insertNewTableRow(filaDestinoActual);
                    if (filaTrabajo.getCtRow() != null && filaEstiloModelo.getCtRow() != null) {
                        filaTrabajo.getCtRow().setTrPr(filaEstiloModelo.getCtRow().getTrPr());
                    }

                    for (int cellIdx = 0; cellIdx < totalColumnasModelo; cellIdx++) {
                        XWPFTableCell celdaOriginal = filaEstiloModelo.getCell(cellIdx);
                        XWPFTableCell celdaNueva = filaTrabajo.createCell();
                        if (celdaOriginal != null && celdaOriginal.getCTTc() != null && celdaNueva.getCTTc() != null && celdaOriginal.getCTTc().getTcPr() != null) {
                            celdaNueva.getCTTc().setTcPr((org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr) celdaOriginal.getCTTc().getTcPr().copy());
                        }
                    }
                }

                String codigoRaSeguro = ra.getReducido() != null ? ra.getReducido() : "";
                String moduloSeguro = ra.getModulo() != null ? ra.getModulo() : "";

                // Recorremos las columnas e inyectamos según el diccionario de memoria protegida
                for (int i = 0; i < totalColumnasModelo; i++) {
                    XWPFTableCell celdaNueva = filaTrabajo.getCell(i);
                    String textoOriginalEtiqueta = etiquetasOriginalesMemoria.get(i);

                    // 1. Columna del Módulo Profesional
                    if (textoOriginalEtiqueta.contains("{{modulo}}") || textoOriginalEtiqueta.contains("MODULO") || textoOriginalEtiqueta.contains("MÓDULO")) {
                        limpiarContenidoCelda(celdaNueva);
                        XWPFParagraph parrafo = celdaNueva.getParagraphs().get(0);
                        XWPFRun run = parrafo.createRun();
                        run.setFontFamily("Arial"); run.setFontSize(10);
                        run.setText(moduloSeguro);
                    } 
                    // 2. Columna del Código Reducido (R.A.)
                    else if (textoOriginalEtiqueta.contains("{{reducido}}") || textoOriginalEtiqueta.contains("CODIGORA") || textoOriginalEtiqueta.contains("RESULTADO")) {
                        limpiarContenidoCelda(celdaNueva);
                        XWPFParagraph parrafo = celdaNueva.getParagraphs().get(0);
                        XWPFRun run = parrafo.createRun();
                        run.setFontFamily("Arial"); run.setFontSize(10); run.setBold(true);
                        run.setText(codigoRaSeguro); // ⚡ Avanza correlativamente de forma real: RA-1, RA-2...
                    } 
                    // 3. 🟢 CASILLAS DE EVALUACIÓN UNICODE: Superado / No superado (Columnas 2 y 3)
                    else if (i == 2 || i == 3) {
                        limpiarContenidoCelda(celdaNueva);
                        XWPFParagraph parrafo = celdaNueva.getParagraphs().get(0);
                        parrafo.setAlignment(ParagraphAlignment.CENTER); // Centrado geométrico perfecto
                        
                        XWPFRun run = parrafo.createRun();
                        run.setFontFamily("MS Gothic"); // Evita que se deforme en la conversión a PDF
                        run.setFontSize(12);
                        run.setText("\u2610"); // 🔲 Cuadradito Unicode nativo manual
                    }
                    // 4. Cualquier otra columna residual o de firmas al final de la tabla
                    else {
                        if (indexRA == 0) {
                            for (XWPFParagraph pCelda : celdaNueva.getParagraphs()) {
                                replaceInParagraph(pCelda, data);

                            }
                        }
                    }
                }
                filaDestinoActual++;}System.out.println("-> [POI] Tabla de valoración del Anexo 9 consolidada al 100% con éxito.");
            }
            return document;
        }
    /**
     * 🕵️ INTERCEPTOR RECURSIVO ESPECÍFICO PARA LAS TABLAS FIJAS Y SUBTABLAS DEL ANEXO 9
     */
private void procesarTablaFijaYAnidadasAnexo9(XWPFTable tabla, Map<String, Object> data) {
    if (tabla.getRows().size() > 0) {
        String textoMuestra = obtenerTextoDeFila(tabla.getRow(0));
        // Si detectamos la sección de la matriz de evaluación del plan de formación, saltamos la tabla
        if (textoMuestra.contains("{{modulo}}") || textoMuestra.contains("PLANDEFORMACIÓN") || textoMuestra.contains("MÓDULO")) {
            return;
        }
    }
    for (XWPFTableRow row : tabla.getRows()) {
        for (XWPFTableCell cell : row.getTableCells()) {
            for (XWPFParagraph p : cell.getParagraphs()) {
                replaceInParagraph(p, data);
            }
            // Si la celda esconde otra mini-tabla interna (tabla anidada), descendemos recursivamente
            if (cell.getTables() != null && !cell.getTables().isEmpty()) {
                for (XWPFTable tablaAnidada : cell.getTables()) {
                    procesarTablaFijaYAnidadasAnexo9(tablaAnidada, data);
                }
            }
        }
    }
}
}
