ReporteService.java

package com.licensis.notaire.service;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.util.JRLoader;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Map;

@Service
public class ReporteService {

    private final DataSource dataSource;

    private static final String RUTA_REPORTE_PRESUPUESTO = "reportes/reportePresupuestoSinInmueble.jasper";
    private static final String RUTA_REPORTE_PRESUPUESTO_INMUEBLES = "reportes/reportePresupuestoInmuebles.jasper";
    private static final String RUTA_REPORTE_LISTA_DOCUMENTOS_TRAMITE = "reportes/reporteListaDocumetosTramite.jasper";
    private static final String RUTA_REPORTE_HISTORIAL_GESTION = "reportes/reporteHistorialGestion.jasper";
    private static final String RUTA_REPORTE_CONSULTAR_VENCIMIENTOS_DOCUMENTOS = "reportes/reporteConsultarVencimientosDocumentos.jasper";
    private static final String RUTA_REPORTE_CONSULTAR_DEUDA_DOCUMENTOS = "reportes/reporteConsultarDeudaDocumentos.jasper";

    public ReporteService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public byte[] generarReportePresupuesto(Integer idPresupuesto) throws Exception {
        Map<String, Object> parameters = Map.of("pIdPresupuesto", idPresupuesto);
        return generarPdfDesdeTemplate(RUTA_REPORTE_PRESUPUESTO, parameters);
    }

    public byte[] generarReportePresupuestoInmuebles(Integer idPresupuesto) throws Exception {
        Map<String, Object> parameters = Map.of("idPresupuestoParam", idPresupuesto);
        return generarPdfDesdeTemplate(RUTA_REPORTE_PRESUPUESTO_INMUEBLES, parameters);
    }

    public byte[] generarReporteListaDocumentosTramite(String nombreTipoTramite) throws Exception {
        Map<String, Object> parameters = Map.of("nombreTipoTramite", nombreTipoTramite);
        return generarPdfDesdeTemplate(RUTA_REPORTE_LISTA_DOCUMENTOS_TRAMITE, parameters);
    }

    public byte[] generarReporteHistorialGestion(Integer idGestion) throws Exception {
        Map<String, Object> parameters = Map.of("idGestion", idGestion);
        return generarPdfDesdeTemplate(RUTA_REPORTE_HISTORIAL_GESTION, parameters);
    }

    public byte[] generarReporteDocumentosPorVencer(Integer idDocumentoPresentado) throws Exception {
        Map<String, Object> parameters = Map.of("idDocumentoPresentado", idDocumentoPresentado);
        return generarPdfDesdeTemplate(RUTA_REPORTE_CONSULTAR_VENCIMIENTOS_DOCUMENTOS, parameters);
    }

    public byte[] generarReporteConsultarDeudaDocumentos(Integer numeroGestion) throws Exception {
        Map<String, Object> parameters = Map.of("numeroGestion", numeroGestion);
        return generarPdfDesdeTemplate(RUTA_REPORTE_CONSULTAR_DEUDA_DOCUMENTOS, parameters);
    }

    public byte[] generarReporteLibroIndice(Integer anio) throws Exception {
        return generarPdfTextoSimple(
                "Libro de Indice",
                "CU24",
                "Periodo: " + anio,
                "Generado: " + LocalDate.now()
        );
    }

    public byte[] generarReporteDeclaracionJuradaMensual(Integer anio, Integer mes) throws Exception {
        return generarPdfTextoSimple(
                "Declaracion Jurada Mensual",
                "CU25",
                "Periodo: " + mes + "/" + anio,
                "Generado: " + LocalDate.now()
        );
    }

    public byte[] generarReporteDeclaracionJuradaRentas(Integer anio, Integer mes) throws Exception {
        return generarPdfTextoSimple(
                "Declaracion Jurada de Rentas",
                "CU50",
                "Periodo: " + mes + "/" + anio,
                "Generado: " + LocalDate.now()
        );
    }

    private byte[] generarPdfDesdeTemplate(String templatePath, Map<String, Object> parameters) throws Exception {
        try (Connection connection = dataSource.getConnection()) {
            InputStream reportStream = getClass().getClassLoader().getResourceAsStream(templatePath);
            if (reportStream == null) {
                throw new RuntimeException("No se encontró el template: " + templatePath);
            }

            JasperReport jasperReport = (JasperReport) JRLoader.loadObject(reportStream);

            JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, connection);

            return JasperExportManager.exportReportToPdf(jasperPrint);
        } catch (SQLException e) {
            throw new RuntimeException("Error de conexión a base de datos: " + e.getMessage(), e);
        } catch (JRException e) {
            throw new RuntimeException("Error al generar el reporte: " + e.getMessage(), e);
        }
    }

    private byte[] generarPdfTextoSimple(String titulo, String cuId, String lineaPeriodo, String lineaFecha) {
        try {
            StringBuilder stream = new StringBuilder();
            stream.append("BT\n");
            stream.append("/F1 20 Tf\n");
            stream.append("50 760 Td\n");
            stream.append("(").append(escapePdfText(titulo)).append(") Tj\n");
            stream.append("/F1 12 Tf\n");
            stream.append("0 -30 Td\n");
            stream.append("(Caso de uso: ").append(escapePdfText(cuId)).append(") Tj\n");
            stream.append("0 -20 Td\n");
            stream.append("(").append(escapePdfText(lineaPeriodo)).append(") Tj\n");
            stream.append("0 -20 Td\n");
            stream.append("(").append(escapePdfText(lineaFecha)).append(") Tj\n");
            stream.append("0 -40 Td\n");
            stream.append("(Reporte operativo generado por backend API.) Tj\n");
            stream.append("ET\n");

            return buildPdf(stream.toString());
        } catch (Exception e) {
            throw new RuntimeException("Error al generar PDF textual: " + e.getMessage(), e);
        }
    }

    private byte[] buildPdf(String contentStream) throws Exception {
        ArrayList<Integer> xref = new ArrayList<>();
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        write(out, "%PDF-1.4\n");

        xref.add(out.size());
        write(out, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");

        xref.add(out.size());
        write(out, "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n");

        xref.add(out.size());
        write(out, "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj\n");

        byte[] streamBytes = contentStream.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
        xref.add(out.size());
        write(out, "4 0 obj\n<< /Length " + streamBytes.length + " >>\nstream\n");
        out.write(streamBytes);
        write(out, "endstream\nendobj\n");

        xref.add(out.size());
        write(out, "5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n");

        int xrefStart = out.size();
        write(out, "xref\n0 6\n");
        write(out, "0000000000 65535 f \n");
        for (Integer offset : xref) {
            write(out, String.format("%010d 00000 n \n", offset));
        }
        write(out, "trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n" + xrefStart + "\n%%EOF");

        return out.toByteArray();
    }

    private void write(ByteArrayOutputStream out, String text) throws Exception {
        out.write(text.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
    }

    private String escapePdfText(String input) {
        return input
                .replace("\\", "\\\\")
                .replace("(", "\\(")
                .replace(")", "\\)");
    }
}