package com.zioinfo.kintex.report;

import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.report.dto.GridPdfRequest;
import net.sf.jasperreports.engine.JasperCompileManager;
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.data.JRMapCollectionDataSource;
import net.sf.jasperreports.engine.design.JRDesignBand;
import net.sf.jasperreports.engine.design.JRDesignExpression;
import net.sf.jasperreports.engine.design.JRDesignField;
import net.sf.jasperreports.engine.design.JRDesignStaticText;
import net.sf.jasperreports.engine.design.JRDesignStyle;
import net.sf.jasperreports.engine.design.JRDesignTextField;
import net.sf.jasperreports.engine.design.JasperDesign;
import net.sf.jasperreports.engine.type.HorizontalTextAlignEnum;
import net.sf.jasperreports.engine.type.ModeEnum;
import net.sf.jasperreports.engine.type.PositionTypeEnum;
import net.sf.jasperreports.engine.type.VerticalTextAlignEnum;
import net.sf.jasperreports.engine.xml.JRXmlLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import java.awt.Color;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 범용 그리드 PDF 렌더 서비스(전 그리드 공용) — WISE/UIWS JasperReports 패턴 이식.
 *
 * <p><b>동적 컬럼 처리:</b> 정적 jrxml({@code reports/grid_report.jrxml})은 로고·타이틀·페이지번호 등
 * "골격"만 담고, 요청 {@link GridPdfRequest#columns()} 개수에 맞춰 {@link JasperDesign} API 로
 * columnHeader(머리글 {@link JRDesignStaticText})·detail(셀 {@link JRDesignTextField}) 요소를 런타임에
 * 주입한 뒤 컴파일한다. Jasper 정적 컬럼 한계를 DynamicJasper 없이 가로 가중/균등 분할 자체 구현으로 회피한다.
 *
 * <p><b>필드 매핑:</b> 컬럼 키가 임의 문자열(공백·중복 가능)이어도 안전하도록 내부 필드명을 {@code c0..cN}
 * 로 재부여하고, 데이터소스 행 맵도 동일 키로 재구성한다(표현식 파손·중복 필드 예외 방지).
 *
 * <p><b>한글:</b> {@code fonts.xml} 폰트확장(NanumGothic, Identity-H, 임베딩)으로 PDF 한글 깨짐 방지.
 * 스타일 {@code fontName="NanumGothic"} 을 머리글/셀 스타일에 지정한다.
 */
@Service
public class GridReportService {

    private static final Logger log = LoggerFactory.getLogger(GridReportService.class);

    private static final String TEMPLATE = "reports/grid_report.jrxml";
    private static final String LOGO = "reports/kintex_ci.jpg";
    private static final DateTimeFormatter TS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    /** A4 가로 컨텐츠 폭(pageWidth 842 - 좌우 여백 30*2). jrxml 과 일치. */
    private static final int CONTENT_WIDTH = 782;
    private static final int HEADER_HEIGHT = 22;
    private static final int ROW_HEIGHT = 16;

    private static final Color BRAND = new Color(0x1F, 0x29, 0xFC);
    private static final Color GRID_LINE = new Color(0xD0, 0xD5, 0xE0);
    private static final Color HEADER_BG = new Color(0x1F, 0x29, 0xFC);

    /** 방어적 상한(요청 DTO 검증과 이중). */
    private static final int MAX_COLUMNS = 30;
    private static final int MAX_ROWS = 5000;

    /**
     * 그리드 요청 → PDF 바이트.
     *
     * @param req 컬럼·행 정의. null·빈 컬럼·초과 크기는 {@code VALIDATION}.
     * @return PDF byte[]
     */
    public byte[] render(GridPdfRequest req) {
        if (req == null || req.columns() == null || req.columns().isEmpty()) {
            throw new ApiException(ErrorCode.VALIDATION, "출력할 컬럼이 없습니다.");
        }
        List<GridPdfRequest.Column> columns = req.columns();
        if (columns.size() > MAX_COLUMNS) {
            throw new ApiException(ErrorCode.VALIDATION, "컬럼은 최대 " + MAX_COLUMNS + "개까지 지원합니다.");
        }
        List<Map<String, Object>> rows = req.rows() == null ? List.of() : req.rows();
        if (rows.size() > MAX_ROWS) {
            throw new ApiException(ErrorCode.VALIDATION, "행은 최대 " + MAX_ROWS + "행까지 지원합니다.");
        }

        try {
            JasperDesign design = loadDesign();
            int[] widths = computeWidths(columns);
            injectColumns(design, columns, widths);

            JasperReport report = JasperCompileManager.compileReport(design);

            Map<String, Object> params = new HashMap<>();
            params.put("P_TITLE", nz(req.title()));
            params.put("P_SUBTITLE", nz(req.subtitle()));
            params.put("P_CREATED_AT", LocalDateTime.now().format(TS));
            params.put("P_TOTAL_ROWS", String.format("%,d", rows.size()));
            InputStream logo = logoStream();
            if (logo != null) {
                params.put("P_LOGO", logo);
            }

            JasperPrint print = JasperFillManager.fillReport(report, params, dataSource(columns, rows));
            return JasperExportManager.exportReportToPdf(print);
        } catch (ApiException e) {
            throw e;
        } catch (Exception e) {
            // 스택트레이스 미노출 — 요약 메시지만(보안 불변).
            log.error("grid pdf render failed: {}", e.getMessage());
            throw new ApiException(ErrorCode.INTERNAL, "PDF 생성에 실패했습니다. 잠시 후 다시 시도해 주세요.");
        }
    }

    // ── 디자인 조립 ───────────────────────────────────────────────────

    private JasperDesign loadDesign() throws Exception {
        // 요청마다 새 디자인(컬럼이 달라 캐시 불가) — 골격 jrxml 을 매번 로드해 주입한다.
        try (InputStream is = new ClassPathResource(TEMPLATE).getInputStream()) {
            return JRXmlLoader.load(is);
        }
    }

    /**
     * 컬럼 폭 산출 — width(가중치)가 있으면 비례, 없으면 균등. 반올림 누적오차는 마지막 컬럼에서 보정.
     */
    private int[] computeWidths(List<GridPdfRequest.Column> columns) {
        int n = columns.size();
        double totalWeight = 0;
        double[] weights = new double[n];
        for (int i = 0; i < n; i++) {
            Integer w = columns.get(i).width();
            double weight = (w != null && w > 0) ? w : 1.0;
            weights[i] = weight;
            totalWeight += weight;
        }
        int[] widths = new int[n];
        int used = 0;
        for (int i = 0; i < n; i++) {
            if (i == n - 1) {
                widths[i] = CONTENT_WIDTH - used; // 마지막 컬럼이 잔여 폭 흡수
            } else {
                widths[i] = (int) Math.round(CONTENT_WIDTH * (weights[i] / totalWeight));
                used += widths[i];
            }
        }
        // 최소 폭 보호(음수/과소 방지) — 극단 가중치 대비.
        for (int i = 0; i < n; i++) {
            if (widths[i] < 20) {
                widths[i] = 20;
            }
        }
        return widths;
    }

    private void injectColumns(JasperDesign design, List<GridPdfRequest.Column> columns, int[] widths)
            throws Exception {
        JRDesignStyle headerStyle = headerStyle();
        JRDesignStyle cellStyle = cellStyle();
        design.addStyle(headerStyle);
        design.addStyle(cellStyle);

        JRDesignBand columnHeader = (JRDesignBand) design.getColumnHeader();
        columnHeader.setHeight(HEADER_HEIGHT);
        JRDesignBand detail = (JRDesignBand) design.getDetailSection().getBands()[0];
        detail.setHeight(ROW_HEIGHT);

        int x = 0;
        for (int i = 0; i < columns.size(); i++) {
            GridPdfRequest.Column col = columns.get(i);
            int w = widths[i];
            String fieldName = "c" + i;

            // 필드 선언(내부 안전 키). 값 클래스 String.
            JRDesignField field = new JRDesignField();
            field.setName(fieldName);
            field.setValueClass(String.class);
            design.addField(field);

            // 머리글(정적 텍스트)
            JRDesignStaticText header = new JRDesignStaticText();
            header.setX(x);
            header.setY(0);
            header.setWidth(w);
            header.setHeight(HEADER_HEIGHT);
            header.setText(nz(col.label()));
            header.setStyle(headerStyle);
            header.setPositionType(PositionTypeEnum.FIX_RELATIVE_TO_TOP);
            columnHeader.addElement(header);

            // 셀(텍스트필드 $F{cN})
            JRDesignTextField cell = new JRDesignTextField();
            cell.setX(x);
            cell.setY(0);
            cell.setWidth(w);
            cell.setHeight(ROW_HEIGHT);
            cell.setStyle(cellStyle);
            cell.setBlankWhenNull(true);
            cell.setStretchWithOverflow(false);
            cell.setHorizontalTextAlign(align(col.align()));
            cell.setPositionType(PositionTypeEnum.FIX_RELATIVE_TO_TOP);
            JRDesignExpression expr = new JRDesignExpression();
            expr.setText("$F{" + fieldName + "}");
            cell.setExpression(expr);
            detail.addElement(cell);

            x += w;
        }
    }

    // ── 스타일 ────────────────────────────────────────────────────────

    private JRDesignStyle headerStyle() {
        JRDesignStyle s = new JRDesignStyle();
        s.setName("gridHeaderStyle");
        s.setFontName("NanumGothic");
        s.setFontSize(Float.valueOf(8.5f));
        s.setBold(true);
        s.setForecolor(Color.WHITE);
        s.setBackcolor(HEADER_BG);
        s.setMode(ModeEnum.OPAQUE);
        s.setHorizontalTextAlign(HorizontalTextAlignEnum.CENTER);
        s.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
        applyBox(s);
        return s;
    }

    private JRDesignStyle cellStyle() {
        JRDesignStyle s = new JRDesignStyle();
        s.setName("gridCellStyle");
        s.setFontName("NanumGothic");
        s.setFontSize(Float.valueOf(8f));
        s.setForecolor(new Color(0x1A, 0x1A, 0x1A));
        s.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
        applyBox(s);
        return s;
    }

    private void applyBox(JRDesignStyle s) {
        s.getLineBox().getPen().setLineWidth(0.5f);
        s.getLineBox().getPen().setLineColor(GRID_LINE);
        s.getLineBox().setLeftPadding(4);
        s.getLineBox().setRightPadding(4);
    }

    private HorizontalTextAlignEnum align(String a) {
        if (a == null) {
            return HorizontalTextAlignEnum.LEFT;
        }
        return switch (a.trim().toLowerCase()) {
            case "center" -> HorizontalTextAlignEnum.CENTER;
            case "right" -> HorizontalTextAlignEnum.RIGHT;
            default -> HorizontalTextAlignEnum.LEFT;
        };
    }

    // ── 데이터소스 ────────────────────────────────────────────────────

    /** 각 행을 내부 필드명(c0..cN)→문자열 값 맵으로 재구성. 누락 키는 빈 문자열. */
    private JRMapCollectionDataSource dataSource(List<GridPdfRequest.Column> columns,
                                                 List<Map<String, Object>> rows) {
        List<Map<String, ?>> data = new ArrayList<>(rows.size());
        for (Map<String, Object> row : rows) {
            Map<String, Object> mapped = new HashMap<>();
            for (int i = 0; i < columns.size(); i++) {
                Object v = row == null ? null : row.get(columns.get(i).key());
                mapped.put("c" + i, stringify(v));
            }
            data.add(mapped);
        }
        return new JRMapCollectionDataSource(data);
    }

    // ── 헬퍼 ──────────────────────────────────────────────────────────

    private InputStream logoStream() {
        try {
            ClassPathResource r = new ClassPathResource(LOGO);
            return r.exists() ? r.getInputStream() : null;
        } catch (Exception e) {
            return null; // 로고 없으면 이미지 미표시(onErrorType="Blank")
        }
    }

    private static String stringify(Object v) {
        return v == null ? "" : String.valueOf(v);
    }

    private static String nz(String s) {
        return s == null ? "" : s;
    }
}
