package com.zioinfo.kintex.document;

import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.document.dto.GeneratedDocDto;
import com.zioinfo.kintex.document.dto.ReportPdfRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * M6 서류 PDF 생성/서빙 서비스(G-05) — 웹폼 데이터를 A4 서식 HTML 로 조립해 PDF 로 렌더한다.
 *
 * <p>렌더러: openhtmltopdf(PDFBox). 한글은 리소스 동봉 폰트({@code /fonts/NanumGothic.ttf})를 임베드해 보존한다
 * (서버에 시스템 폰트가 없어도 안정). 파일 저장은 로그인 슬라이드/아바타와 동일 컨벤션:
 * {@code {kintex.upload.dir}/documents/{eventId}/} + <b>서버 UUID 파일명</b>. 원본/클라이언트 경로는 신뢰하지
 * 않으므로 경로 순회는 구조적으로 불가능하다. 다운로드는 인증 엔드포인트(GET)로 바이트 스트림한다.
 *
 * <p>HWP 는 스코프 밖(후속). 사용자 입력은 모두 HTML escape 하여 서식에 주입한다(XSS/서식 훼손 방지).
 */
@Service
public class DocumentPdfService {

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

    /** 리소스 동봉 한글 폰트(임베드). 정적 상수 폰트 패밀리로 서식 전체에 적용. */
    private static final String FONT_RESOURCE = "/fonts/NanumGothic.ttf";
    private static final String FONT_FAMILY = "Nanum Gothic";

    private final DocumentMapper mapper;
    private final Path documentsDir;

    public DocumentPdfService(DocumentMapper mapper,
                              @Value("${kintex.upload.dir:./data/uploads}") String uploadDir) {
        this.mapper = mapper;
        this.documentsDir = Paths.get(uploadDir).toAbsolutePath().normalize().resolve("documents");
    }

    // ── 생성: 신고서류(안전관리 계획서) ─────────────────────────────────

    /** SCR-23 웹폼 → 안전관리 계획서 PDF. form 은 미완성(빈 값 다수)이어도 미리보기 PDF 를 발급한다. */
    public GeneratedDocDto generateReport(String eventId, String docType, ReportPdfRequest form) {
        String title = blankTo(form == null ? null : form.docTitle(), "재해대처계획서");
        String html = buildReportHtml(title, form == null ? new ReportPdfRequest(
                null, null, null, null, null, null, null, null, null, null, null, null) : form);
        byte[] pdf = renderPdf(html);
        String fileName = safeFileBase(title) + "_" + shortDate() + ".pdf";
        return store(eventId, pdf, fileName);
    }

    // ── 생성: 서류·마일스톤 현황 요약 ─────────────────────────────────

    /** SCR-22 "자동 문서 생성" → 서류 준비 현황 요약 PDF(서버 저장 데이터로 조립, 폼 입력 불필요). */
    public GeneratedDocDto generateSummary(String eventId) {
        List<Map<String, Object>> milestones = mapper.findMilestones(eventId);
        List<Map<String, Object>> documents = mapper.findDocuments(eventId);
        List<Map<String, Object>> issues = mapper.findReviewIssues(eventId);
        String html = buildSummaryHtml(milestones, documents, issues);
        byte[] pdf = renderPdf(html);
        String fileName = "서류_준비현황_요약_" + shortDate() + ".pdf";
        return store(eventId, pdf, fileName);
    }

    // ── 서빙(다운로드) ────────────────────────────────────────────────

    /** 저장된 PDF 바이트 로드. fileId 는 서버 UUID(.pdf) — 안전성 검증 후 실재 파일만 통과. 없으면 404. */
    public byte[] load(String eventId, String fileId) {
        if (!isSafeName(fileId) || !fileId.toLowerCase().endsWith(".pdf")) {
            throw new ApiException(ErrorCode.NOT_FOUND, "요청한 문서를 찾을 수 없습니다.");
        }
        Path dir = eventDir(eventId);
        Path target = dir.resolve(fileId).normalize();
        if (!target.startsWith(dir) || !Files.isRegularFile(target)) {
            throw new ApiException(ErrorCode.NOT_FOUND, "요청한 문서를 찾을 수 없습니다.");
        }
        try {
            return Files.readAllBytes(target);
        } catch (IOException e) {
            log.warn("document pdf load failed: {}", e.getMessage());
            throw new ApiException(ErrorCode.NOT_FOUND, "요청한 문서를 찾을 수 없습니다.");
        }
    }

    // ── 렌더/저장 ─────────────────────────────────────────────────────

    private byte[] renderPdf(String html) {
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            PdfRendererBuilder builder = new PdfRendererBuilder();
            builder.useFont(() -> fontStream(), FONT_FAMILY);
            builder.withHtmlContent(html, null);
            builder.toStream(os);
            builder.run();
            return os.toByteArray();
        } catch (Exception e) {
            // 스택트레이스 미노출 — 요약만 전달(보안 불변).
            log.error("document pdf render failed: {}", e.getMessage());
            throw new ApiException(ErrorCode.INTERNAL, "PDF 생성에 실패했습니다. 잠시 후 다시 시도해 주세요.");
        }
    }

    private InputStream fontStream() {
        InputStream in = DocumentPdfService.class.getResourceAsStream(FONT_RESOURCE);
        if (in == null) {
            throw new ApiException(ErrorCode.INTERNAL, "PDF 서식 폰트를 불러올 수 없습니다.");
        }
        return in;
    }

    private GeneratedDocDto store(String eventId, byte[] pdf, String fileName) {
        String fileId = UUID.randomUUID() + ".pdf"; // 서버 발급 식별자(원본명 미신뢰)
        try {
            Path dir = eventDir(eventId);
            Files.createDirectories(dir);
            Path target = dir.resolve(fileId).normalize();
            if (!target.startsWith(dir)) { // UUID라 사실상 불변식이나 방어적 확인
                throw new ApiException(ErrorCode.INTERNAL, "잘못된 저장 경로입니다.");
            }
            Files.write(target, pdf);
        } catch (IOException e) {
            log.error("document pdf store failed: {}", e.getMessage());
            throw new ApiException(ErrorCode.INTERNAL, "PDF 저장에 실패했습니다.");
        }
        String url = "/api/events/" + eventId + "/documents/files/" + fileId;
        return new GeneratedDocDto(fileId, fileName, url);
    }

    private Path eventDir(String eventId) {
        // eventId 는 경로 세그먼트로 쓰이므로 안전성 확인(순회/구분자 차단).
        if (!isSafeName(eventId)) {
            throw new ApiException(ErrorCode.VALIDATION, "잘못된 행사 식별자입니다.");
        }
        return documentsDir.resolve(eventId).normalize();
    }

    // ── HTML 서식 ─────────────────────────────────────────────────────

    private String buildReportHtml(String title, ReportPdfRequest f) {
        String visitors = numOrDash(f.visitors());
        StringBuilder b = new StringBuilder();
        openHtml(b, esc(title));
        b.append("<h1 class=\"title\">").append(esc(title)).append("</h1>");

        b.append("<table class=\"meta\"><tbody>");
        b.append("<tr><th>행 사 명</th><td colspan=\"3\">").append(dash(f.eventName())).append("</td></tr>");
        b.append("<tr><th>일 시</th><td>").append(fmtDate(f.eventDate()))
                .append("</td><th>장 소</th><td>").append(dash(f.venue())).append("</td></tr>");
        b.append("<tr><th>주최/주관</th><td>주식회사 킨텍스 조직위원회</td>")
                .append("<th>예상인원</th><td>").append(visitors.equals("—") ? "—" : visitors + "명").append("</td></tr>");
        b.append("</tbody></table>");

        b.append("<h2>1. 안전관리 조직체계</h2><div class=\"box\">");
        b.append("총괄 안전책임자 ").append(dash2(f.safetyManager(), "(미지정)"));
        if (f.safetyPhone() != null && !f.safetyPhone().isBlank()) {
            b.append(" (").append(esc(f.safetyPhone())).append(")");
        }
        b.append(" 지휘 하에 현장 안전요원 ").append(dash2(f.guardCount(), "0"))
                .append("인을 배치한다. 각 홀 출입구에 안전요원을 상주시켜 밀집도를 관리하며, ")
                .append("정기 장내 방송으로 안전 수칙을 안내한다.</div>");

        b.append("<h2>2. 비상 연락 체계</h2><ul>");
        b.append("<li>관할 소방서: ").append(dash(f.fireStation())).append("</li>");
        b.append("<li>관할 경찰서: ").append(dash(f.policeStation())).append("</li>");
        b.append("<li>의무·응급: ").append(dash(f.medical())).append("</li>");
        b.append("</ul>");

        b.append("<h2>3. 주요 재해대처 방안</h2><ul>");
        b.append("<li>화재 시: 소방시설 즉시 가동 및 안내 방송을 통한 관람객 대피 유도</li>");
        b.append("<li>응급환자 발생 시: 현장 의무실 이송 및 119 구급대 협조 요청</li>");
        b.append("<li>정전 시: 비상 발전기 가동 및 유도등 점등 확인</li>");
        b.append("</ul>");

        b.append("<h2>4. 위험물 관리</h2><div class=\"box\">")
                .append(dash2(f.hazardous(), "해당 없음")).append("</div>");

        b.append("<div class=\"sign\"><div class=\"sign-date\">").append(fmtDate(f.eventDate())).append("</div>");
        b.append("<div class=\"sign-row\"><span>주식회사 킨텍스 대표이사</span>")
                .append("<span class=\"stamp\">KINTEX<br/>직인</span></div></div>");

        closeHtml(b);
        return b.toString();
    }

    private String buildSummaryHtml(List<Map<String, Object>> milestones,
                                    List<Map<String, Object>> documents,
                                    List<Map<String, Object>> issues) {
        StringBuilder b = new StringBuilder();
        openHtml(b, "서류 준비 현황 요약");
        b.append("<h1 class=\"title\">서류 준비 현황 요약</h1>");
        b.append("<p class=\"gen\">생성일: ").append(LocalDate.now()).append("</p>");

        // 마일스톤
        b.append("<h2>1. 전시 마일스톤</h2>");
        if (milestones == null || milestones.isEmpty()) {
            b.append("<div class=\"box\">구성된 마일스톤이 없습니다.</div>");
        } else {
            b.append("<table class=\"grid\"><thead><tr><th>단계</th><th>상태</th><th>기한</th></tr></thead><tbody>");
            for (Map<String, Object> m : milestones) {
                b.append("<tr><td>").append(dash(str(m.get("label"))))
                        .append("</td><td>").append(stateLabel(str(m.get("state"))))
                        .append("</td><td>").append(dash(str(m.get("dueDate")))).append("</td></tr>");
            }
            b.append("</tbody></table>");
        }

        // 서류 체크리스트 + 공정률
        int total = documents == null ? 0 : documents.size();
        long done = documents == null ? 0 : documents.stream()
                .filter(d -> !"pending".equals(String.valueOf(d.get("status")))).count();
        int pct = total == 0 ? 0 : (int) Math.round(done * 100.0 / total);
        b.append("<h2>2. 신고서류 체크리스트 (진척 ").append(pct).append("%)</h2>");
        if (total == 0) {
            b.append("<div class=\"box\">구성된 서류가 없습니다.</div>");
        } else {
            b.append("<table class=\"grid\"><thead><tr><th>서류</th><th>상태</th><th>기한</th></tr></thead><tbody>");
            for (Map<String, Object> d : documents) {
                b.append("<tr><td>").append(dash(str(d.get("name"))))
                        .append("</td><td>").append(docStatusLabel(str(d.get("status"))))
                        .append("</td><td>").append(dash(str(d.get("dueDate")))).append("</td></tr>");
            }
            b.append("</tbody></table>");
        }

        // AI 검수 이슈
        b.append("<h2>3. AI 서류 검수</h2>");
        if (issues == null || issues.isEmpty()) {
            b.append("<div class=\"box\">검출된 불일치·누락이 없습니다.</div>");
        } else {
            b.append("<ul>");
            for (Map<String, Object> i : issues) {
                b.append("<li><b>").append(dash(str(i.get("title")))).append("</b> — ")
                        .append(dash(str(i.get("description")))).append("</li>");
            }
            b.append("</ul>");
        }

        b.append("<p class=\"note\">본 문서는 킨텍스 AI 전시·행사시스템이 자동 생성한 준비 현황 요약입니다. ")
                .append("공식 제출은 각 서류를 kxwp 시스템에 업로드하는 방식입니다.</p>");
        closeHtml(b);
        return b.toString();
    }

    private void openHtml(StringBuilder b, String docTitle) {
        b.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        b.append("<html><head><meta charset=\"UTF-8\"/><title>").append(docTitle).append("</title>");
        b.append("<style>");
        b.append("@page{size:A4;margin:2cm;}");
        b.append("*{font-family:'").append(FONT_FAMILY).append("',sans-serif;box-sizing:border-box;}");
        b.append("body{color:#1a1a1a;font-size:11pt;line-height:1.6;}");
        b.append("h1.title{text-align:center;font-size:18pt;margin:0 0 6pt;padding-bottom:8pt;border-bottom:2pt solid #1f29fc;}");
        b.append(".gen,.note{color:#666;font-size:9pt;}");
        b.append(".note{margin-top:18pt;padding-top:8pt;border-top:1pt solid #ccc;}");
        b.append("h2{font-size:12.5pt;margin:16pt 0 6pt;color:#1f29fc;}");
        b.append("table{width:100%;border-collapse:collapse;margin:4pt 0;}");
        b.append("table.meta th{background:#f2f4f8;width:18%;text-align:center;}");
        b.append("table.meta th,table.meta td{border:1pt solid #b8bfce;padding:6pt 8pt;font-size:10.5pt;}");
        b.append("table.grid th{background:#1f29fc;color:#fff;padding:6pt 8pt;font-size:10pt;}");
        b.append("table.grid td{border:1pt solid #d0d5e0;padding:5pt 8pt;font-size:10pt;}");
        b.append(".box{border:1pt solid #d0d5e0;background:#fafbfd;padding:8pt 10pt;border-radius:2pt;}");
        b.append("ul{margin:4pt 0;padding-left:18pt;}li{margin:2pt 0;}");
        b.append(".sign{margin-top:28pt;text-align:center;}");
        b.append(".sign-date{margin-bottom:10pt;font-size:11pt;}");
        b.append(".sign-row{display:inline-block;}");
        b.append(".sign-row span{font-size:12pt;font-weight:bold;margin-right:10pt;}");
        b.append(".stamp{display:inline-block;border:1.5pt solid #c0392b;color:#c0392b;border-radius:50%;");
        b.append("width:52pt;height:52pt;line-height:1.1;font-size:8pt;text-align:center;padding-top:12pt;}");
        b.append("</style></head><body>");
    }

    private void closeHtml(StringBuilder b) {
        b.append("</body></html>");
    }

    // ── 파생/포맷 헬퍼 ────────────────────────────────────────────────

    private static String fmtDate(String v) {
        if (v == null || v.isBlank()) {
            return "—";
        }
        String[] p = v.split("-");
        if (p.length == 3) {
            return esc(p[0] + ". " + p[1] + ". " + p[2] + ".");
        }
        return esc(v);
    }

    private static String stateLabel(String state) {
        return switch (state == null ? "" : state) {
            case "done" -> "완료";
            case "active" -> "진행중";
            default -> "대기";
        };
    }

    private static String docStatusLabel(String status) {
        return switch (status == null ? "" : status) {
            case "approved" -> "승인";
            case "submitted" -> "제출";
            case "draft" -> "작성중";
            case "rejected" -> "반려";
            default -> "준비중";
        };
    }

    private static String shortDate() {
        return LocalDate.now().toString().replace("-", "");
    }

    /** 파일명 base(한글 허용, 경로/구분자·제어문자만 제거). */
    private static String safeFileBase(String s) {
        if (s == null || s.isBlank()) {
            return "document";
        }
        String cleaned = s.replaceAll("[\\\\/:*?\"<>|\\r\\n\\t]", "").trim();
        return cleaned.isEmpty() ? "document" : cleaned;
    }

    private static boolean isSafeName(String name) {
        return name != null && !name.isBlank()
                && !name.contains("/") && !name.contains("\\") && !name.contains("..");
    }

    private static String blankTo(String v, String def) {
        return (v == null || v.isBlank()) ? def : v;
    }

    /** escape + 빈값 → "—". */
    private static String dash(String v) {
        return (v == null || v.isBlank()) ? "—" : esc(v);
    }

    /** escape + 빈값 → 지정 기본값. */
    private static String dash2(String v, String def) {
        return (v == null || v.isBlank()) ? esc(def) : esc(v);
    }

    private static String numOrDash(String v) {
        if (v == null || v.isBlank()) {
            return "—";
        }
        try {
            return String.format("%,d", Long.parseLong(v.trim()));
        } catch (NumberFormatException e) {
            return esc(v);
        }
    }

    private static String str(Object o) {
        return o == null ? null : String.valueOf(o);
    }

    /** HTML/XML escape — 사용자 입력을 서식에 주입하기 전 필수(XHTML well-formed 보존). */
    private static String esc(String s) {
        if (s == null) {
            return "";
        }
        StringBuilder out = new StringBuilder(s.length() + 16);
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            switch (c) {
                case '&' -> out.append("&amp;");
                case '<' -> out.append("&lt;");
                case '>' -> out.append("&gt;");
                case '"' -> out.append("&quot;");
                case '\'' -> out.append("&#39;");
                default -> out.append(c);
            }
        }
        return out.toString();
    }
}
