package com.writeoff.module.meeting.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.writeoff.module.file.service.OssService;
import org.apache.poi.ooxml.POIXMLException;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
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.CTRPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

@Service
public class MeetingSummaryExportService {
    private static final String TEMPLATE_PATH = "templates/meeting-summary-template.docx";
    private static final String THEME_IMAGE_MARKER = "${themeCaption}";
    private static final String EXPERT_IMAGE_MARKER = "${chairCaption}";
    private static final String MATERIAL_IMAGE_MARKER = "${materialCaption}";
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final DateTimeFormatter DOC_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy年M月d日");
    private static final DateTimeFormatter DOC_TIME_FORMATTER = DateTimeFormatter.ofPattern("H:mm");
    private static final int SECTION_IMAGE_MAX_WIDTH = 520;
    private static final int SECTION_IMAGE_MAX_HEIGHT = 360;
    private static final int EXPERT_IMAGE_MAX_WIDTH = 440;
    private static final int EXPERT_IMAGE_MAX_HEIGHT = 320;

    private final JdbcTemplate jdbcTemplate;
    private final OssService ossService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public MeetingSummaryExportService(JdbcTemplate jdbcTemplate, OssService ossService) {
        this.jdbcTemplate = jdbcTemplate;
        this.ossService = ossService;
    }

    public byte[] buildDocx(Long tenantId, Long meetingId) {
        SummaryContext context = loadSummaryContext(tenantId, meetingId);
        try (XWPFDocument document = openTemplateDocument();
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            AnchorCells anchorCells = locateAnchorCells(document);
            replacePlaceholders(document, buildPlaceholderValues(context));
            renderImageSection(anchorCells.themeCell, context.themeImages, false, SECTION_IMAGE_MAX_WIDTH, SECTION_IMAGE_MAX_HEIGHT);
            renderImageSection(anchorCells.expertCell, context.expertImages, true, EXPERT_IMAGE_MAX_WIDTH, EXPERT_IMAGE_MAX_HEIGHT);
            renderImageSection(anchorCells.materialCell, context.materialImages, false, SECTION_IMAGE_MAX_WIDTH, SECTION_IMAGE_MAX_HEIGHT);
            document.write(outputStream);
            return outputStream.toByteArray();
        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "会议总结生成失败");
        }
    }

    private SummaryContext loadSummaryContext(Long tenantId, Long meetingId) {
        Map<String, Object> meeting = findMeeting(tenantId, meetingId);
        Map<String, String> materialJsonByCode = loadMaterialJsonByCode(tenantId, meetingId);
        String basicInfoJson = materialJsonByCode.get("BASIC_INFO");
        if (basicInfoJson == null || basicInfoJson.trim().isEmpty()) {
            throw new BusinessException(
                    ErrorCodes.VALIDATION_ERROR,
                    "会议基本信息未保存，请先在会议资料-会议基本信息中保存后再生成总结"
            );
        }

        Map<String, Object> basicInfo = parseJsonMap(basicInfoJson);
        Map<String, Object> writeOffDocs = parseJsonMap(materialJsonByCode.get("WRITE_OFF_DOCS"));
        Map<String, Object> expertList = parseJsonMap(materialJsonByCode.get("EXPERT_LIST"));
        Map<String, Object> meetingInvoice = parseJsonMap(materialJsonByCode.get("MEETING_INVOICE"));
        List<Map<String, Object>> expertBindings = jdbcTemplate.queryForList(
                "SELECT expert_id, expert_name, title, organization FROM meeting_expert_binding " +
                        "WHERE tenant_id=? AND meeting_id=? ORDER BY id ASC",
                tenantId,
                meetingId
        );

        Map<Long, ExpertProfile> expertProfileById = new LinkedHashMap<Long, ExpertProfile>();
        Map<String, ExpertProfile> expertProfileByName = new LinkedHashMap<String, ExpertProfile>();
        Map<Long, String> expertNameById = new LinkedHashMap<Long, String>();
        for (Map<String, Object> expertBinding : expertBindings) {
            Long expertId = toLong(expertBinding.get("expert_id"));
            String expertName = stringValue(expertBinding.get("expert_name"));
            String organization = firstNonEmptyText(
                    stringValue(expertBinding.get("organization")),
                    stringValue(expertBinding.get("title"))
            );
            ExpertProfile profile = new ExpertProfile(expertId, expertName, organization);
            if (expertId != null && expertId > 0L) {
                expertNameById.put(expertId, expertName);
                expertProfileById.put(expertId, profile);
            }
            if (!expertName.isEmpty()) {
                expertProfileByName.put(normalizeKey(expertName), profile);
            }
        }

        List<Long> chairmanIds = parseIdList(basicInfo.get("chairmanExpertIds"));
        List<Long> speakerIds = parseIdList(basicInfo.get("speakerExpertIds"));
        List<Long> hostIds = parseIdList(basicInfo.get("hostExpertIds"));
        List<Long> discussionGuestIds = parseIdList(basicInfo.get("discussionGuestExpertIds"));

        List<String> chairmanNames = resolveExpertNames(chairmanIds, expertNameById);
        List<String> speakerNames = resolveExpertNames(speakerIds, expertNameById);
        List<String> hostNames = resolveExpertNames(hostIds, expertNameById);
        List<String> discussionGuestNames = resolveExpertNames(discussionGuestIds, expertNameById);
        markExpertRoles(chairmanIds, expertProfileById, "大会主席");
        markExpertRoles(hostIds, expertProfileById, "会议主持");
        markExpertRoles(speakerIds, expertProfileById, "会议讲者");

        markExpertRoles(discussionGuestIds, expertProfileById, "讨论嘉宾");

        String meetingTopic = stringValue(meeting.get("topic"));
        String projectName = stringValue(meeting.get("project_name"));
        String organizationName = firstNonEmptyText(
                stringValue(meeting.get("host_enterprise_name")),
                projectName,
                queryTenantName(tenantId)
        );
        String meetingCategory = stringValue(meeting.get("meeting_category"));
        String location = stringValue(meeting.get("location"));
        String startTime = stringValue(meeting.get("start_time"));
        String endTime = stringValue(meeting.get("end_time"));
        String guestCountText = formatNumber(basicInfo.get("guestCount"));
        if ("-".equals(guestCountText) && !expertBindings.isEmpty()) {
            guestCountText = String.valueOf(expertBindings.size());
        }

        SummaryContext context = new SummaryContext();
        context.summaryTitle = firstNonEmptyText(meetingTopic, projectName, "会议") + "-会议总结";
        context.organizationName = firstNonEmptyText(organizationName, "-");
        context.meetingCategory = firstNonEmptyText(meetingCategory, "-");
        context.meetingTime = formatMeetingTime(startTime, endTime);
        context.meetingLocation = firstNonEmptyText(location, "-");
        context.meetingTopic = firstNonEmptyText(meetingTopic, "-");
        context.chairAndSpeaker = firstNonEmptyText(
                buildRoleSummaryText(chairmanNames, hostNames, speakerNames, discussionGuestNames),
                "-"
        );
        context.guests = firstNonEmptyText(guestCountText, "-");
        context.attendeePlan = formatNumber(basicInfo.get("attendeeCount"));
        context.attendeeActual = formatNumber(basicInfo.get("attendeeActualCount"));
        context.targetAudience = firstNonEmptyText(normalizePlainText(stringValue(basicInfo.get("targetAudience"))), "-");
        context.mainAgenda = firstNonEmptyText(normalizePlainText(stringValue(basicInfo.get("mainAgenda"))), "-");
        context.meetingEffect = firstNonEmptyText(normalizePlainText(stringValue(basicInfo.get("meetingEffect"))), "-");
        context.improvementSuggestion = firstNonEmptyText(
                normalizePlainText(stringValue(basicInfo.get("improvementSuggestion"))),
                "无"
        );
        context.themeInstruction = "（含" + firstNonEmptyText(meetingTopic, projectName, "会议主题") + "）画面或会议横幅";
        context.footerDate = formatFooterDate(firstNonEmptyText(endTime, startTime));
        context.themeImages = collectThemeImages(writeOffDocs);
        context.expertImages = collectExpertImages(expertList, expertNameById, expertProfileById, expertProfileByName);
        context.materialImages = collectMaterialImages(meetingInvoice);
        return context;
    }

    private Map<String, Object> findMeeting(Long tenantId, Long meetingId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT m.id, m.topic, m.meeting_category, m.location, " +
                        "DATE_FORMAT(m.start_time, '%Y-%m-%d %H:%i:%s') AS start_time, " +
                        "DATE_FORMAT(m.end_time, '%Y-%m-%d %H:%i:%s') AS end_time, " +
                        "p.project_name, p.host_enterprise_name " +
                        "FROM meeting m " +
                        "LEFT JOIN project p ON m.tenant_id=p.tenant_id AND m.project_id=p.id " +
                        "WHERE m.tenant_id=? AND m.id=? AND m.is_deleted=0 LIMIT 1",
                tenantId,
                meetingId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "会议不存在");
        }
        return rows.get(0);
    }

    private Map<String, String> loadMaterialJsonByCode(Long tenantId, Long meetingId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT module_code, content_json FROM meeting_material WHERE tenant_id=? AND meeting_id=? AND is_deleted=0",
                tenantId,
                meetingId
        );
        Map<String, String> result = new LinkedHashMap<String, String>();
        for (Map<String, Object> row : rows) {
            String moduleCode = stringValue(row.get("module_code"));
            if (!moduleCode.isEmpty()) {
                result.put(moduleCode, stringValue(row.get("content_json")));
            }
        }
        return result;
    }

    private Map<String, Object> parseJsonMap(String contentJson) {
        if (contentJson == null || contentJson.trim().isEmpty()) {
            return new LinkedHashMap<String, Object>();
        }
        try {
            return objectMapper.readValue(contentJson, new TypeReference<Map<String, Object>>() {});
        } catch (Exception ex) {
            return new LinkedHashMap<String, Object>();
        }
    }

    private String queryTenantName(Long tenantId) {
        try {
            String name = jdbcTemplate.queryForObject(
                    "SELECT tenant_name FROM tenant WHERE id=? AND is_deleted=0 LIMIT 1",
                    String.class,
                    tenantId
            );
            return name == null ? "" : name.trim();
        } catch (Exception ex) {
            return "";
        }
    }

    private List<Long> parseIdList(Object value) {
        if (!(value instanceof List)) {
            return Collections.emptyList();
        }
        List<?> list = (List<?>) value;
        List<Long> result = new ArrayList<Long>();
        for (Object item : list) {
            Long id = toLong(item);
            if (id != null && id > 0L) {
                result.add(id);
            }
        }
        return result;
    }

    private List<String> resolveExpertNames(List<Long> expertIds, Map<Long, String> expertNameById) {
        Set<String> dedup = new LinkedHashSet<String>();
        for (Long expertId : expertIds) {
            String name = expertNameById.get(expertId);
            if (name != null && !name.trim().isEmpty()) {
                dedup.add(name.trim());
            }
        }
        return new ArrayList<String>(dedup);
    }

    private List<ImageSource> collectThemeImages(Map<String, Object> writeOffDocs) {
        List<ImageSource> result = new ArrayList<ImageSource>();
        Set<String> usedKeys = new LinkedHashSet<String>();
        Map<String, Object> themePhoto = mapValue(writeOffDocs.get("themePhoto"));
        String themePhotoKey = stringValue(themePhoto.get("ossKey"));
        String themePhotoName = firstNonEmptyText(
                stringValue(themePhoto.get("name")),
                stringValue(themePhoto.get("fileName")),
                fileNameFromObjectKey(themePhotoKey)
        );
        if (!themePhotoKey.isEmpty() && isImageFile(themePhotoName, themePhotoKey) && usedKeys.add(themePhotoKey)) {
            result.add(new ImageSource(themePhotoKey, themePhotoName, ""));
        }
        if (result.isEmpty()) {
            for (Map<String, Object> agenda : listOfMap(writeOffDocs.get("agenda"))) {
                String objectKey = stringValue(agenda.get("ossKey"));
                String fileName = firstNonEmptyText(
                        stringValue(agenda.get("name")),
                        fileNameFromObjectKey(objectKey)
                );
                if (objectKey.isEmpty() || !isImageFile(fileName, objectKey) || !usedKeys.add(objectKey)) {
                    continue;
                }
                result.add(new ImageSource(objectKey, fileName, ""));
            }
        }
        return result;
    }

    private List<ImageSource> collectExpertImages(Map<String, Object> expertList, Map<Long, String> expertNameById) {
        List<ImageSource> result = new ArrayList<ImageSource>();
        Set<String> usedKeys = new LinkedHashSet<String>();
        Map<String, Object> onsitePhoto = mapValue(expertList.get("onsitePhoto"));
        for (Map<String, Object> photo : listOfMap(onsitePhoto.get("photos"))) {
            String objectKey = stringValue(photo.get("ossKey"));
            String fileName = firstNonEmptyText(
                    stringValue(photo.get("name")),
                    fileNameFromObjectKey(objectKey)
            );
            if (objectKey.isEmpty() || !isImageFile(fileName, objectKey) || !usedKeys.add(objectKey)) {
                continue;
            }
            Long expertId = toLong(photo.get("expertId"));
            String expertName = firstNonEmptyText(
                    stringValue(photo.get("expertName")),
                    expertId == null ? "" : stringValue(expertNameById.get(expertId)),
                    "未命名专家"
            );
            result.add(new ImageSource(objectKey, fileName, expertName));
        }
        return result;
    }

    private List<ImageSource> collectExpertImages(Map<String, Object> expertList,
                                                  Map<Long, String> expertNameById,
                                                  Map<Long, ExpertProfile> expertProfileById,
                                                  Map<String, ExpertProfile> expertProfileByName) {
        List<ImageSource> result = new ArrayList<ImageSource>();
        Set<String> usedKeys = new LinkedHashSet<String>();
        Map<String, Object> onsitePhoto = mapValue(expertList.get("onsitePhoto"));
        for (Map<String, Object> photo : listOfMap(onsitePhoto.get("photos"))) {
            String objectKey = stringValue(photo.get("ossKey"));
            String fileName = firstNonEmptyText(
                    stringValue(photo.get("name")),
                    fileNameFromObjectKey(objectKey)
            );
            if (objectKey.isEmpty() || !isImageFile(fileName, objectKey) || !usedKeys.add(objectKey)) {
                continue;
            }
            Long expertId = toLong(photo.get("expertId"));
            String expertName = firstNonEmptyText(
                    stringValue(photo.get("expertName")),
                    expertId == null ? "" : stringValue(expertNameById.get(expertId)),
                    "未命名专家"
            );
            ExpertProfile profile = resolveExpertProfile(expertId, expertName, expertProfileById, expertProfileByName);
            result.add(new ImageSource(objectKey, fileName, buildExpertPhotoCaption(expertName, profile)));
        }
        return result;
    }

    private void markExpertRoles(List<Long> expertIds, Map<Long, ExpertProfile> expertProfileById, String roleLabel) {
        for (Long expertId : expertIds == null ? Collections.<Long>emptyList() : expertIds) {
            if (expertId == null) {
                continue;
            }
            ExpertProfile profile = expertProfileById.get(expertId);
            if (profile != null) {
                profile.addRole(roleLabel);
            }
        }
    }

    private ExpertProfile resolveExpertProfile(Long expertId,
                                               String expertName,
                                               Map<Long, ExpertProfile> expertProfileById,
                                               Map<String, ExpertProfile> expertProfileByName) {
        if (expertId != null) {
            ExpertProfile profile = expertProfileById.get(expertId);
            if (profile != null) {
                return profile;
            }
        }
        if (expertName == null || expertName.trim().isEmpty()) {
            return null;
        }
        return expertProfileByName.get(normalizeKey(expertName));
    }

    private String buildExpertPhotoCaption(String fallbackName, ExpertProfile profile) {
        String expertName = firstNonEmptyText(
                profile == null ? "" : profile.name,
                fallbackName,
                "未命名专家"
        );
        String roles = profile == null ? "" : joinDisplayNames(new ArrayList<String>(profile.roles));
        String organization = profile == null ? "" : profile.organization;
        List<String> parts = new ArrayList<String>();
        parts.add(expertName);
        if (!roles.isEmpty()) {
            parts.add(roles);
        }
        if (!organization.isEmpty()) {
            parts.add(organization);
        }
        return String.join(" / ", parts);
    }

    private List<ImageSource> collectMaterialImages(Map<String, Object> meetingInvoice) {
        List<ImageSource> result = new ArrayList<ImageSource>();
        Set<String> usedKeys = new LinkedHashSet<String>();
        for (Map<String, Object> section : listOfMap(meetingInvoice.get("sections"))) {
            if (!"MATERIAL_DETAIL".equalsIgnoreCase(stringValue(section.get("sectionCode")))) {
                continue;
            }
            for (Map<String, Object> file : listOfMap(section.get("files"))) {
                String objectKey = stringValue(file.get("ossKey"));
                String fileName = firstNonEmptyText(
                        stringValue(file.get("fileName")),
                        stringValue(file.get("name")),
                        fileNameFromObjectKey(objectKey)
                );
                if (objectKey.isEmpty() || !isImageFile(fileName, objectKey) || !usedKeys.add(objectKey)) {
                    continue;
                }
                result.add(new ImageSource(objectKey, fileName, ""));
            }
        }
        return result;
    }

    private XWPFDocument openTemplateDocument() {
        ClassPathResource resource = new ClassPathResource(TEMPLATE_PATH);
        try (InputStream inputStream = resource.getInputStream()) {
            return new XWPFDocument(inputStream);
        } catch (IOException ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "会议总结模板损坏");
        } catch (POIXMLException ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "会议总结模板损坏");
        }
    }

    private AnchorCells locateAnchorCells(XWPFDocument document) {
        XWPFTableCell themeCell = null;
        XWPFTableCell expertCell = null;
        XWPFTableCell materialCell = null;
        for (XWPFTable table : document.getTables()) {
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    String text = stringValue(cell.getText());
                    if (themeCell == null && text.contains(THEME_IMAGE_MARKER)) {
                        themeCell = cell;
                    }
                    if (expertCell == null && text.contains(EXPERT_IMAGE_MARKER)) {
                        expertCell = cell;
                    }
                    if (materialCell == null && text.contains(MATERIAL_IMAGE_MARKER)) {
                        materialCell = cell;
                    }
                }
            }
        }
        if (themeCell == null || expertCell == null || materialCell == null) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "会议总结模板损坏");
        }
        return new AnchorCells(themeCell, expertCell, materialCell);
    }

    private Map<String, String> buildPlaceholderValues(SummaryContext context) {
        Map<String, String> values = new LinkedHashMap<String, String>();
        values.put("${summaryTitle}", context.summaryTitle);
        values.put("${organizationName}", context.organizationName);
        values.put("${meetingCategory}", context.meetingCategory);
        values.put("${meetingTime}", context.meetingTime);
        values.put("${meetingLocation}", context.meetingLocation);
        values.put("${meetingTopic}", context.meetingTopic);
        values.put("${chairAndSpeaker}", context.chairAndSpeaker);
        values.put("${guests}", context.guests);
        values.put("${attendeePlan}", context.attendeePlan);
        values.put("${attendeeActual}", context.attendeeActual);
        values.put("${targetAudience}", context.targetAudience);
        values.put("${mainAgenda}", context.mainAgenda);
        values.put("${meetingEffect}", context.meetingEffect);
        values.put("${improvementSuggestion}", context.improvementSuggestion);
        values.put("${themeInstruction}", context.themeInstruction);
        values.put("${themeCaption}", "");
        values.put("${chairCaption}", "");
        values.put("${hostCaption}", "");
        values.put("${speakerCaption}", "");
        values.put("${discussionCaption}", "");
        values.put("${materialCaption}", "");
        values.put("${footerDate}", context.footerDate);
        return values;
    }

    private void replacePlaceholders(XWPFDocument document, Map<String, String> placeholders) {
        for (XWPFParagraph paragraph : document.getParagraphs()) {
            replacePlaceholders(paragraph, placeholders);
        }
        for (XWPFTable table : document.getTables()) {
            replacePlaceholders(table, placeholders);
        }
    }

    private void replacePlaceholders(XWPFTable table, Map<String, String> placeholders) {
        for (XWPFTableRow row : table.getRows()) {
            for (XWPFTableCell cell : row.getTableCells()) {
                for (XWPFParagraph paragraph : cell.getParagraphs()) {
                    replacePlaceholders(paragraph, placeholders);
                }
                for (XWPFTable nestedTable : cell.getTables()) {
                    replacePlaceholders(nestedTable, placeholders);
                }
            }
        }
    }

    private void replacePlaceholders(XWPFParagraph paragraph, Map<String, String> placeholders) {
        String original = paragraph.getParagraphText();
        if (original == null || original.isEmpty()) {
            return;
        }
        String replaced = original;
        for (Map.Entry<String, String> entry : placeholders.entrySet()) {
            if (replaced.contains(entry.getKey())) {
                replaced = replaced.replace(entry.getKey(), safeValue(entry.getValue()));
            }
        }
        if (!original.equals(replaced)) {
            rewriteParagraph(paragraph, replaced);
        }
    }

    private void rewriteParagraph(XWPFParagraph paragraph, String text) {
        CTRPr style = null;
        if (!paragraph.getRuns().isEmpty() && paragraph.getRuns().get(0).getCTR().isSetRPr()) {
            style = (CTRPr) paragraph.getRuns().get(0).getCTR().getRPr().copy();
        }
        for (int i = paragraph.getRuns().size() - 1; i >= 0; i--) {
            paragraph.removeRun(i);
        }
        XWPFRun run = paragraph.createRun();
        if (style != null) {
            run.getCTR().setRPr(style);
        }
        setRunText(run, safeValue(text));
    }

    private void renderImageSection(XWPFTableCell cell,
                                    List<ImageSource> sources,
                                    boolean showCaption,
                                    int maxWidthPx,
                                    int maxHeightPx) throws IOException, InvalidFormatException {
        List<RenderableImage> images = loadRenderableImages(sources, maxWidthPx, maxHeightPx);
        int skippedCount = Math.max(0, sources.size() - images.size());
        XWPFParagraph firstParagraph = clearCell(cell);
        if (images.isEmpty()) {
            String message = sources.isEmpty() ? "暂无图片" : "图片加载失败或格式暂不支持";
            writeParagraph(firstParagraph, message, ParagraphAlignment.CENTER, 11, false);
            return;
        }

        boolean useFirstParagraph = true;
        for (RenderableImage image : images) {
            XWPFParagraph imageParagraph = useFirstParagraph ? firstParagraph : cell.addParagraph();
            useFirstParagraph = false;
            imageParagraph.setAlignment(ParagraphAlignment.CENTER);
            imageParagraph.setSpacingAfter(80);
            XWPFRun imageRun = imageParagraph.createRun();
            imageRun.addPicture(
                    new ByteArrayInputStream(image.bytes),
                    image.pictureType,
                    image.fileName,
                    Units.pixelToEMU(image.widthPx),
                    Units.pixelToEMU(image.heightPx)
            );

            if (showCaption && !image.caption.isEmpty()) {
                XWPFParagraph captionParagraph = cell.addParagraph();
                captionParagraph.setAlignment(ParagraphAlignment.CENTER);
                captionParagraph.setSpacingAfter(180);
                writeParagraph(captionParagraph, image.caption, ParagraphAlignment.CENTER, 10, false);
            } else {
                imageParagraph.setSpacingAfter(180);
            }
        }

        if (skippedCount > 0) {
            XWPFParagraph noteParagraph = cell.addParagraph();
            writeParagraph(noteParagraph, "部分图片因格式不支持或文件损坏未导入。", ParagraphAlignment.CENTER, 9, true);
        }
    }

    private List<RenderableImage> loadRenderableImages(List<ImageSource> sources, int maxWidthPx, int maxHeightPx) {
        List<RenderableImage> result = new ArrayList<RenderableImage>();
        for (ImageSource source : sources == null ? Collections.<ImageSource>emptyList() : sources) {
            RenderableImage image = loadRenderableImage(source, maxWidthPx, maxHeightPx);
            if (image != null) {
                result.add(image);
            }
        }
        return result;
    }

    private RenderableImage loadRenderableImage(ImageSource source, int maxWidthPx, int maxHeightPx) {
        if (source == null || source.objectKey.isEmpty()) {
            return null;
        }
        try {
            byte[] objectBytes = ossService.getObjectBytes(source.objectKey);
            if (objectBytes == null || objectBytes.length == 0) {
                return null;
            }
            return buildRenderableImage(objectBytes, source.fileName, source.caption, maxWidthPx, maxHeightPx);
        } catch (Exception ex) {
            return null;
        }
    }

    private RenderableImage buildRenderableImage(byte[] sourceBytes,
                                                 String fileName,
                                                 String caption,
                                                 int maxWidthPx,
                                                 int maxHeightPx) throws IOException {
        String effectiveFileName = firstNonEmptyText(fileName, "image");
        Integer pictureType = resolvePictureType(effectiveFileName);
        byte[] effectiveBytes = sourceBytes;

        if (pictureType == null) {
            BufferedImage image = ImageIO.read(new ByteArrayInputStream(sourceBytes));
            if (image == null) {
                return null;
            }
            byte[] converted = writeImageBytes(image, "png");
            if (converted == null || converted.length == 0) {
                return null;
            }
            effectiveBytes = converted;
            effectiveFileName = replaceFileExtension(effectiveFileName, "png");
            pictureType = XWPFDocument.PICTURE_TYPE_PNG;
        }

        BufferedImage effectiveImage = ImageIO.read(new ByteArrayInputStream(effectiveBytes));
        if (effectiveImage == null) {
            return null;
        }
        ImageSize imageSize = scaleImage(effectiveImage.getWidth(), effectiveImage.getHeight(), maxWidthPx, maxHeightPx);
        return new RenderableImage(
                effectiveBytes,
                pictureType.intValue(),
                effectiveFileName,
                safeValue(caption),
                imageSize.width,
                imageSize.height
        );
    }

    private byte[] writeImageBytes(BufferedImage image, String formatName) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        if (!ImageIO.write(image, formatName, outputStream)) {
            return null;
        }
        return outputStream.toByteArray();
    }

    private ImageSize scaleImage(int widthPx, int heightPx, int maxWidthPx, int maxHeightPx) {
        if (widthPx <= 0 || heightPx <= 0) {
            return new ImageSize(Math.max(1, maxWidthPx), Math.max(1, maxHeightPx));
        }
        double ratio = Math.min(
                Math.min((double) maxWidthPx / (double) widthPx, (double) maxHeightPx / (double) heightPx),
                1D
        );
        int scaledWidth = Math.max(1, (int) Math.round(widthPx * ratio));
        int scaledHeight = Math.max(1, (int) Math.round(heightPx * ratio));
        return new ImageSize(scaledWidth, scaledHeight);
    }

    private XWPFParagraph clearCell(XWPFTableCell cell) {
        int paragraphCount = cell.getParagraphs().size();
        for (int i = paragraphCount - 1; i >= 0; i--) {
            cell.removeParagraph(i);
        }
        CTTc ctTc = cell.getCTTc();
        for (int i = ctTc.sizeOfTblArray() - 1; i >= 0; i--) {
            ctTc.removeTbl(i);
        }
        return cell.addParagraph();
    }

    private void writeParagraph(XWPFParagraph paragraph,
                                String text,
                                ParagraphAlignment alignment,
                                int fontSize,
                                boolean italic) {
        paragraph.setAlignment(alignment);
        XWPFRun run = paragraph.createRun();
        if (fontSize > 0) {
            run.setFontSize(fontSize);
        }
        run.setItalic(italic);
        setRunText(run, text);
    }

    private void setRunText(XWPFRun run, String text) {
        String[] lines = safeValue(text).split("\n", -1);
        if (lines.length == 0) {
            run.setText("", 0);
            return;
        }
        run.setText(lines[0], 0);
        for (int i = 1; i < lines.length; i++) {
            run.addBreak();
            run.setText(lines[i]);
        }
    }

    private Integer resolvePictureType(String fileName) {
        String suffix = fileExtension(fileName);
        if ("png".equals(suffix)) {
            return Integer.valueOf(XWPFDocument.PICTURE_TYPE_PNG);
        }
        if ("jpg".equals(suffix) || "jpeg".equals(suffix)) {
            return Integer.valueOf(XWPFDocument.PICTURE_TYPE_JPEG);
        }
        if ("gif".equals(suffix)) {
            return Integer.valueOf(XWPFDocument.PICTURE_TYPE_GIF);
        }
        if ("bmp".equals(suffix)) {
            return Integer.valueOf(XWPFDocument.PICTURE_TYPE_BMP);
        }
        return null;
    }

    private String replaceFileExtension(String fileName, String targetExtension) {
        String cleanName = firstNonEmptyText(fileName, "image");
        int dotIndex = cleanName.lastIndexOf('.');
        String baseName = dotIndex >= 0 ? cleanName.substring(0, dotIndex) : cleanName;
        return baseName + "." + targetExtension;
    }

    private String fileExtension(String fileName) {
        String cleanName = stringValue(fileName).toLowerCase(Locale.ROOT);
        int dotIndex = cleanName.lastIndexOf('.');
        if (dotIndex < 0 || dotIndex >= cleanName.length() - 1) {
            return "";
        }
        return cleanName.substring(dotIndex + 1);
    }

    private String formatMeetingTime(String startTimeText, String endTimeText) {
        LocalDateTime start = parseDateTime(startTimeText);
        LocalDateTime end = parseDateTime(endTimeText);
        if (start == null && end == null) {
            return "-";
        }
        if (start != null && end != null) {
            if (start.toLocalDate().equals(end.toLocalDate())) {
                return DOC_DATE_FORMATTER.format(start.toLocalDate()) + " "
                        + DOC_TIME_FORMATTER.format(start) + "~" + DOC_TIME_FORMATTER.format(end);
            }
            return DOC_DATE_FORMATTER.format(start.toLocalDate()) + " " + DOC_TIME_FORMATTER.format(start)
                    + " ~ "
                    + DOC_DATE_FORMATTER.format(end.toLocalDate()) + " " + DOC_TIME_FORMATTER.format(end);
        }
        LocalDateTime only = start != null ? start : end;
        return DOC_DATE_FORMATTER.format(only.toLocalDate()) + " " + DOC_TIME_FORMATTER.format(only);
    }

    private String formatFooterDate(String dateTimeText) {
        LocalDateTime dateTime = parseDateTime(dateTimeText);
        LocalDate date = dateTime == null ? LocalDate.now() : dateTime.toLocalDate();
        return DOC_DATE_FORMATTER.format(date);
    }

    private LocalDateTime parseDateTime(String text) {
        if (text == null || text.trim().isEmpty()) {
            return null;
        }
        try {
            return LocalDateTime.parse(text.trim(), DATE_TIME_FORMATTER);
        } catch (Exception ex) {
            return null;
        }
    }

    private String normalizePlainText(String value) {
        if (value == null || value.trim().isEmpty()) {
            return "";
        }
        String normalized = value.replace("\r\n", "\n").replace("\r", "\n");
        String[] parts = normalized.split("\n");
        List<String> items = new ArrayList<String>();
        for (String part : parts) {
            String text = part == null ? "" : part.trim();
            if (!text.isEmpty()) {
                items.add(text);
            }
        }
        return items.isEmpty() ? "" : String.join("；", items);
    }

    private String buildRoleSummaryText(List<String> chairmanNames,
                                        List<String> hostNames,
                                        List<String> speakerNames,
                                        List<String> discussionGuestNames) {
        List<String> parts = new ArrayList<String>();
        String chairmanText = joinDisplayNames(chairmanNames);
        String hostText = joinDisplayNames(hostNames);
        String speakerText = joinDisplayNames(speakerNames);
        String discussionGuestText = joinDisplayNames(discussionGuestNames);
        if (!chairmanText.isEmpty()) {
            parts.add("主席：" + chairmanText);
        }
        if (!hostText.isEmpty()) {
            parts.add("主持：" + hostText);
        }
        if (!speakerText.isEmpty()) {
            parts.add("讲者：" + speakerText);
        }
        if (!discussionGuestText.isEmpty()) {
            parts.add("讨论嘉宾：" + discussionGuestText);
        }
        return parts.isEmpty() ? "" : String.join("；", parts);
    }

    private String joinDisplayNames(List<String> values) {
        List<String> filtered = new ArrayList<String>();
        for (String value : values == null ? Collections.<String>emptyList() : values) {
            if (value != null && !value.trim().isEmpty()) {
                filtered.add(value.trim());
            }
        }
        return filtered.isEmpty() ? "" : String.join("、", filtered);
    }

    private String normalizeKey(String value) {
        return stringValue(value).replace(" ", "").toLowerCase(Locale.ROOT);
    }

    private String firstNonEmptyText(String... values) {
        for (String value : values) {
            if (value != null && !value.trim().isEmpty()) {
                return value.trim();
            }
        }
        return "";
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> listOfMap(Object value) {
        if (!(value instanceof List)) {
            return Collections.emptyList();
        }
        List<?> list = (List<?>) value;
        List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
        for (Object item : list) {
            if (item instanceof Map) {
                result.add((Map<String, Object>) item);
            }
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> mapValue(Object value) {
        if (value instanceof Map) {
            return (Map<String, Object>) value;
        }
        return Collections.emptyMap();
    }

    private boolean isImageFile(String fileName, String objectKey) {
        String name = stringValue(fileName).toLowerCase(Locale.ROOT);
        String key = stringValue(objectKey).toLowerCase(Locale.ROOT);
        return name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".jpeg")
                || name.endsWith(".gif") || name.endsWith(".bmp") || name.endsWith(".webp")
                || key.endsWith(".png") || key.endsWith(".jpg") || key.endsWith(".jpeg")
                || key.endsWith(".gif") || key.endsWith(".bmp") || key.endsWith(".webp");
    }

    private String fileNameFromObjectKey(String objectKey) {
        String key = stringValue(objectKey);
        if (key.isEmpty()) {
            return "";
        }
        int idx = key.lastIndexOf('/');
        return idx >= 0 ? key.substring(idx + 1) : key;
    }

    private String formatNumber(Object value) {
        Long number = toLong(value);
        return number == null ? "-" : String.valueOf(number);
    }

    private String stringValue(Object value) {
        return value == null ? "" : String.valueOf(value).trim();
    }

    private String safeValue(String value) {
        return value == null ? "" : value;
    }

    private Long toLong(Object value) {
        if (value instanceof Number) {
            return ((Number) value).longValue();
        }
        try {
            String text = stringValue(value);
            return text.isEmpty() ? null : Long.valueOf(text);
        } catch (Exception ex) {
            return null;
        }
    }

    private static final class SummaryContext {
        private String summaryTitle;
        private String organizationName;
        private String meetingCategory;
        private String meetingTime;
        private String meetingLocation;
        private String meetingTopic;
        private String chairAndSpeaker;
        private String guests;
        private String attendeePlan;
        private String attendeeActual;
        private String targetAudience;
        private String mainAgenda;
        private String meetingEffect;
        private String improvementSuggestion;
        private String themeInstruction;
        private String footerDate;
        private List<ImageSource> themeImages = Collections.emptyList();
        private List<ImageSource> expertImages = Collections.emptyList();
        private List<ImageSource> materialImages = Collections.emptyList();
    }

    private static final class AnchorCells {
        private final XWPFTableCell themeCell;
        private final XWPFTableCell expertCell;
        private final XWPFTableCell materialCell;

        private AnchorCells(XWPFTableCell themeCell, XWPFTableCell expertCell, XWPFTableCell materialCell) {
            this.themeCell = themeCell;
            this.expertCell = expertCell;
            this.materialCell = materialCell;
        }
    }

    private static final class ImageSource {
        private final String objectKey;
        private final String fileName;
        private final String caption;

        private ImageSource(String objectKey, String fileName, String caption) {
            this.objectKey = objectKey == null ? "" : objectKey.trim();
            this.fileName = fileName == null ? "" : fileName.trim();
            this.caption = caption == null ? "" : caption.trim();
        }
    }

    private static final class ExpertProfile {
        private final Long expertId;
        private final String name;
        private final String organization;
        private final LinkedHashSet<String> roles = new LinkedHashSet<String>();

        private ExpertProfile(Long expertId, String name, String organization) {
            this.expertId = expertId;
            this.name = name == null ? "" : name.trim();
            this.organization = organization == null ? "" : organization.trim();
        }

        private void addRole(String roleLabel) {
            if (roleLabel != null && !roleLabel.trim().isEmpty()) {
                roles.add(roleLabel.trim());
            }
        }
    }

    private static final class RenderableImage {
        private final byte[] bytes;
        private final int pictureType;
        private final String fileName;
        private final String caption;
        private final int widthPx;
        private final int heightPx;

        private RenderableImage(byte[] bytes, int pictureType, String fileName, String caption, int widthPx, int heightPx) {
            this.bytes = bytes;
            this.pictureType = pictureType;
            this.fileName = fileName;
            this.caption = caption;
            this.widthPx = widthPx;
            this.heightPx = heightPx;
        }
    }

    private static final class ImageSize {
        private final int width;
        private final int height;

        private ImageSize(int width, int height) {
            this.width = width;
            this.height = height;
        }
    }
}
