package com.writeoff.module.meeting.service;

import com.writeoff.common.api.PageResult;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.module.export.dto.CreateExportTaskRequest;
import com.writeoff.module.export.service.ExportTaskService;
import com.writeoff.module.file.service.OssService;
import com.writeoff.module.meeting.dto.SaveMeetingMaterialRequest;
import com.writeoff.module.meeting.dto.SubmitMeetingMaterialRequest;
import com.writeoff.module.meeting.model.Meeting;
import com.writeoff.module.meeting.model.MeetingMaterial;
import com.writeoff.module.meeting.model.MeetingMaterialHistory;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.writeoff.security.AuthContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.math.BigDecimal;
import java.math.RoundingMode;

@Service
public class MeetingMaterialService {
    private static final Set<String> SUPPORTED_MODULES = new HashSet<>(Arrays.asList("BASIC_INFO", "WRITE_OFF_DOCS", "EXPERT_PROFILE", "EXPERT_LIST", "MEETING_INVOICE"));
    private static final Set<String> MEETING_INVOICE_AMOUNT_SECTION_CODES = new HashSet<>(Arrays.asList(
            "VENUE_CONFIRMATION",
            "CONSTRUCTION_DETAIL",
            "ACCOMMODATION_DETAIL",
            "CATERING_DETAIL",
            "LOCAL_TRANSPORT_DETAIL",
            "INTERCITY_TRANSPORT_DETAIL",
            "MATERIAL_DETAIL",
            "DESIGN_DRAFT_DETAIL"
    ));
    private static final List<String> MEETING_INVOICE_SECTION_CODES = Arrays.asList(
            "SETTLEMENT_DETAIL",
            "VENUE_CONFIRMATION",
            "CONSTRUCTION_DETAIL",
            "ACCOMMODATION_DETAIL",
            "CATERING_DETAIL",
            "LOCAL_TRANSPORT_DETAIL",
            "INTERCITY_TRANSPORT_DETAIL",
            "MATERIAL_DETAIL",
            "DESIGN_DRAFT_DETAIL",
            "OTHER"
    );
    private static final Map<String, List<String>> MEETING_INVOICE_SECTION_FIELD_KEYS = new LinkedHashMap<String, List<String>>();
    static {
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("SETTLEMENT_DETAIL", Arrays.asList("settlementFile"));
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("VENUE_CONFIRMATION", Arrays.asList("invoiceFile", "detailFile"));
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("CONSTRUCTION_DETAIL", Arrays.asList("invoiceFile", "detailFile", "equipmentFile"));
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("ACCOMMODATION_DETAIL", Arrays.asList("invoiceFile", "detailFile"));
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("CATERING_DETAIL", Arrays.asList("invoiceFile", "detailFile"));
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("LOCAL_TRANSPORT_DETAIL", Arrays.asList("invoiceFile", "detailFile"));
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("INTERCITY_TRANSPORT_DETAIL", Arrays.asList("invoiceFile", "detailFile"));
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("MATERIAL_DETAIL", Arrays.asList("invoiceFile", "detailFile", "materialFile"));
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("DESIGN_DRAFT_DETAIL", Arrays.asList("invoiceFile", "detailFile", "designDraftFile"));
        MEETING_INVOICE_SECTION_FIELD_KEYS.put("OTHER", Arrays.asList("invoiceFile", "detailFile", "equipmentFile", "materialFile", "designDraftFile"));
    }
    private final JdbcTemplate jdbcTemplate;
    private final MeetingService meetingService;
    private final ExportTaskService exportTaskService;
    private final OssService ossService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    private static final RowMapper<MeetingMaterial> MATERIAL_ROW_MAPPER = (rs, n) -> new MeetingMaterial(
            rs.getLong("id"),
            rs.getLong("meeting_id"),
            rs.getString("module_code"),
            rs.getString("content_json"),
            rs.getString("status"),
            rs.getString("audit_node_status"),
            rs.getString("audit_aggregate_status"),
            rs.getString("submit_remark"),
            rs.getInt("reject_count"),
            rs.getString("last_reject_reason"),
            rs.getString("resubmit_at"),
            rs.getInt("version_no"),
            rs.getInt("is_latest_version") == 1,
            rs.getString("updated_at")
    );

    private static final RowMapper<MeetingMaterialHistory> HISTORY_ROW_MAPPER = (rs, n) -> new MeetingMaterialHistory(
            rs.getLong("id"),
            rs.getLong("meeting_id"),
            rs.getString("module_code"),
            rs.getInt("version_no"),
            rs.getString("action_type"),
            rs.getString("content_json"),
            rs.getString("remark"),
            rs.getString("created_at")
    );

    public MeetingMaterialService(JdbcTemplate jdbcTemplate, MeetingService meetingService, ExportTaskService exportTaskService, OssService ossService) {
        this.jdbcTemplate = jdbcTemplate;
        this.meetingService = meetingService;
        this.exportTaskService = exportTaskService;
        this.ossService = ossService;
    }

    public PageResult<MeetingMaterial> list(Long meetingId) {
        meetingService.getById(meetingId);
        List<MeetingMaterial> list = jdbcTemplate.query(
                "SELECT id, meeting_id, module_code, content_json, status, " +
                        "audit_node_status, audit_aggregate_status, submit_remark, reject_count, last_reject_reason, " +
                        "DATE_FORMAT(resubmit_at, '%Y-%m-%d %H:%i:%s') AS resubmit_at, version_no, is_latest_version, " +
                        "DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at " +
                        "FROM meeting_material WHERE tenant_id=? AND meeting_id=? AND is_deleted=0 ORDER BY id ASC",
                MATERIAL_ROW_MAPPER,
                tenantId(),
                meetingId
        );
        return new PageResult<>(list, list.size(), 1, 20);
    }

    public MeetingMaterial current(Long meetingId, String moduleCode) {
        meetingService.getById(meetingId);
        validateModule(moduleCode);
        String storageModuleCode = resolveStorageModuleCode(moduleCode);
        List<MeetingMaterial> list = jdbcTemplate.query(
                "SELECT id, meeting_id, module_code, content_json, status, " +
                        "audit_node_status, audit_aggregate_status, submit_remark, reject_count, last_reject_reason, " +
                        "DATE_FORMAT(resubmit_at, '%Y-%m-%d %H:%i:%s') AS resubmit_at, version_no, is_latest_version, " +
                        "DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at " +
                        "FROM meeting_material WHERE tenant_id=? AND meeting_id=? AND module_code=? AND is_deleted=0 LIMIT 1",
                MATERIAL_ROW_MAPPER,
                tenantId(),
                meetingId,
                storageModuleCode
        );
        return list.isEmpty() ? null : list.get(0);
    }

    public String currentContentJsonOrEmpty(Long meetingId, String moduleCode) {
        MeetingMaterial material = current(meetingId, moduleCode);
        if (material == null || material.getContentJson() == null) {
            return "";
        }
        return material.getContentJson();
    }

    @Transactional
    public MeetingMaterial save(Long meetingId, String moduleCode, SaveMeetingMaterialRequest request) {
        validateMeetingBudgetLimit(meetingId, moduleCode, request.getContentJson());
        return upsert(meetingId, moduleCode, request.getContentJson(), request.getRemark(), "SAVE", "DRAFT");
    }

    @Transactional
    public MeetingMaterial saveRawContent(Long meetingId, String moduleCode, String contentJson, String remark) {
        validateMeetingBudgetLimit(meetingId, moduleCode, contentJson);
        return upsert(meetingId, moduleCode, contentJson, remark, "SAVE", "DRAFT");
    }

    @Transactional
    public MeetingMaterial submit(Long meetingId, String moduleCode, SubmitMeetingMaterialRequest request) {
        validateSubmitContent(moduleCode, request.getContentJson());
        validateMeetingBudgetLimit(meetingId, moduleCode, request.getContentJson());
        return upsert(meetingId, moduleCode, request.getContentJson(), request.getRemark(), "SUBMIT", "SUBMITTED");
    }

    public PageResult<MeetingMaterialHistory> history(Long meetingId, String moduleCode) {
        meetingService.getById(meetingId);
        validateModule(moduleCode);
        String storageModuleCode = resolveStorageModuleCode(moduleCode);
        List<MeetingMaterialHistory> list = jdbcTemplate.query(
                "SELECT id, meeting_id, module_code, version_no, action_type, content_json, remark, " +
                        "DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS created_at " +
                        "FROM meeting_material_history WHERE tenant_id=? AND meeting_id=? AND module_code=? ORDER BY version_no DESC",
                HISTORY_ROW_MAPPER,
                tenantId(),
                meetingId,
                storageModuleCode
        );
        return new PageResult<>(list, list.size(), 1, 20);
    }

    public List<Map<String, Object>> listMaterialReviewItems(Long meetingId, String moduleCode) {
        MeetingMaterial material = current(meetingId, moduleCode);
        if (material == null || material.getContentJson() == null || material.getContentJson().trim().isEmpty()) {
            return Collections.emptyList();
        }
        return buildReviewItems(moduleCode, material.getContentJson());
    }

    public List<Map<String, Object>> listMaterialItemReviews(Long taskId, String reviewNode, String moduleCode) {
        validateModule(moduleCode);
        return jdbcTemplate.query(
                "SELECT item_key, item_label, review_result, review_reason, reviewer_user_id, " +
                        "DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at " +
                        "FROM audit_material_item_review " +
                        "WHERE tenant_id=? AND task_id=? AND review_node=? AND module_code=? ORDER BY item_key ASC",
                (rs, n) -> {
                    Map<String, Object> row = new LinkedHashMap<>();
                    row.put("itemKey", rs.getString("item_key"));
                    row.put("itemLabel", rs.getString("item_label"));
                    row.put("reviewResult", rs.getString("review_result"));
                    row.put("reviewReason", rs.getString("review_reason"));
                    row.put("reviewerUserId", rs.getLong("reviewer_user_id"));
                    row.put("updatedAt", rs.getString("updated_at"));
                    return row;
                },
                tenantId(),
                taskId,
                reviewNode,
                moduleCode
        );
    }

    @Transactional
    public int saveMaterialItemReviewRecords(Long meetingId,
                                             Long taskId,
                                             String reviewNode,
                                             String moduleCode,
                                             List<Map<String, Object>> items,
                                             String reviewResult,
                                             String reviewReason,
                                             Long reviewerUserId) {
        validateModule(moduleCode);
        if (items == null || items.isEmpty()) {
            return 0;
        }
        int affected = 0;
        for (Map<String, Object> item : items) {
            String itemKey = stringValue(item.get("itemKey"));
            String itemLabel = stringValue(item.get("itemLabel"));
            if (itemKey == null || itemKey.isEmpty() || itemLabel == null || itemLabel.isEmpty()) {
                continue;
            }
            affected += jdbcTemplate.update(
                    "INSERT INTO audit_material_item_review (tenant_id, meeting_id, task_id, review_node, module_code, item_key, item_label, review_result, review_reason, reviewer_user_id) " +
                            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
                            "ON DUPLICATE KEY UPDATE item_label=VALUES(item_label), review_result=VALUES(review_result), review_reason=VALUES(review_reason), reviewer_user_id=VALUES(reviewer_user_id), updated_at=CURRENT_TIMESTAMP",
                    tenantId(),
                    meetingId,
                    taskId,
                    reviewNode,
                    moduleCode,
                    itemKey,
                    itemLabel,
                    reviewResult,
                    reviewReason,
                    reviewerUserId == null ? 0L : reviewerUserId
            );
        }
        return affected;
    }

    public Map<String, Object> createMaterialsExportTask(Long meetingId, String idempotencyKey, String fileName) {
        Meeting meeting = meetingService.getById(meetingId);
        CreateExportTaskRequest request = new CreateExportTaskRequest();
        request.setIdempotencyKey(idempotencyKey);
        request.setTaskCode("MEETING_MATERIAL_EXPORT");
        request.setBizType("MEETING_MATERIAL");
        request.setBizId(String.valueOf(meetingId));
        request.setFiltersJson("{\"meetingId\":" + meetingId + "}");
        request.setFileName(resolveMeetingExportFileName(meeting, fileName, "会议资料包", ".zip", "meeting-material-"));
        return exportTaskService.create(request);
    }

    public Map<String, Object> generateSummaryTask(Long meetingId, String idempotencyKey, String fileName) {
        Meeting meeting = meetingService.getById(meetingId);
        CreateExportTaskRequest request = new CreateExportTaskRequest();
        request.setIdempotencyKey(idempotencyKey);
        request.setTaskCode("MEETING_SUMMARY_GENERATE");
        request.setBizType("MEETING_SUMMARY");
        request.setBizId(String.valueOf(meetingId));
        request.setFiltersJson("{\"meetingId\":" + meetingId + "}");
        request.setFileName(resolveMeetingExportFileName(meeting, fileName, "会议总结", ".docx", "meeting-summary-"));
        return exportTaskService.create(request);
    }

    private String resolveMeetingExportFileName(Meeting meeting,
                                                String requestedFileName,
                                                String suffixLabel,
                                                String extension,
                                                String genericPrefix) {
        String normalizedExtension = normalizeExtension(extension);
        if (isMeaningfulExportFileName(requestedFileName, normalizedExtension, genericPrefix)) {
            return ensureFileExtension(sanitizeFileName(requestedFileName), normalizedExtension);
        }
        String meetingTopic = meeting == null ? "" : stringValue(meeting.getTopic());
        String baseName = sanitizeFileName(firstNonEmptyText(meetingTopic, "会议"));
        return ensureFileExtension(baseName + "-" + suffixLabel, normalizedExtension);
    }

    private boolean isMeaningfulExportFileName(String fileName, String extension, String genericPrefix) {
        String text = sanitizeFileName(fileName);
        if (text.isEmpty()) {
            return false;
        }
        String lower = text.toLowerCase(Locale.ROOT);
        String normalizedExtension = normalizeExtension(extension).toLowerCase(Locale.ROOT);
        if (!lower.endsWith(normalizedExtension)) {
            return false;
        }
        String prefix = genericPrefix == null ? "" : genericPrefix.trim().toLowerCase(Locale.ROOT);
        return prefix.isEmpty() || !lower.startsWith(prefix);
    }

    private String ensureFileExtension(String fileName, String extension) {
        String safeName = sanitizeFileName(fileName);
        String normalizedExtension = normalizeExtension(extension);
        if (safeName.isEmpty()) {
            return "文件" + normalizedExtension;
        }
        if (safeName.toLowerCase(Locale.ROOT).endsWith(normalizedExtension.toLowerCase(Locale.ROOT))) {
            return safeName;
        }
        return safeName + normalizedExtension;
    }

    private String normalizeExtension(String extension) {
        String text = stringValue(extension);
        if (text.isEmpty()) {
            return "";
        }
        return text.startsWith(".") ? text : "." + text;
    }

    private String sanitizeFileName(String value) {
        String text = stringValue(value).replaceAll("[\\\\/:*?\"<>|]", "_");
        text = text.replaceAll("\\s+", " ").trim();
        if (text.isEmpty()) {
            return "";
        }
        return text.length() > 120 ? text.substring(0, 120).trim() : text;
    }

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

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

    public Map<String, Object> getSummaryTaskStatus(Long meetingId, Long taskId) {
        meetingService.getById(meetingId);
        List<Map<String, Object>> rows;
        List<Object> args = new ArrayList<Object>();
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT id, file_name, status, file_oss_key, download_token, retry_count, max_retry, ");
        sql.append("DATE_FORMAT(download_token_expire_at, '%Y-%m-%d %H:%i:%s') AS expire_at, ");
        sql.append("error_message, DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS created_at, ");
        sql.append("DATE_FORMAT(finished_at, '%Y-%m-%d %H:%i:%s') AS finished_at ");
        sql.append("FROM export_task ");
        sql.append("WHERE tenant_id=? AND biz_type='MEETING_SUMMARY' AND biz_id=? AND requested_by=? AND is_deleted=0 ");
        args.add(tenantId());
        args.add(String.valueOf(meetingId));
        args.add(safeUserId());
        if (taskId != null && taskId > 0L) {
            sql.append("AND id=? ");
            args.add(taskId);
        }
        sql.append("ORDER BY id DESC LIMIT 1");
        rows = jdbcTemplate.queryForList(sql.toString(), args.toArray());
        if (rows.isEmpty()) {
            throw new BusinessException(10003, "会议总结任务不存在");
        }
        Map<String, Object> row = rows.get(0);
        row = reconcileFailedSummaryTask(row);
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("taskId", row.get("id"));
        data.put("fileName", row.get("file_name"));
        data.put("status", row.get("status"));
        data.put("fileOssKey", row.get("file_oss_key"));
        data.put("downloadToken", row.get("download_token"));
        data.put("expireAt", row.get("expire_at"));
        data.put("errorMessage", row.get("error_message"));
        data.put("createdAt", row.get("created_at"));
        data.put("finishedAt", row.get("finished_at"));
        return data;
    }

    public Map<String, Object> refreshSummaryToken(Long meetingId, Long taskId) {
        meetingService.getById(meetingId);
        if (taskId == null || taskId <= 0L) {
            throw new BusinessException(10001, "会议总结任务ID不能为空");
        }
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM export_task WHERE tenant_id=? AND id=? AND biz_type='MEETING_SUMMARY' AND biz_id=? AND requested_by=? AND is_deleted=0",
                Integer.class,
                tenantId(),
                taskId,
                String.valueOf(meetingId),
                safeUserId()
        );
        if (count == null || count == 0) {
            throw new BusinessException(10003, "会议总结任务不存在");
        }
        return exportTaskService.refreshDownloadToken(taskId);
    }

    public Map<String, Object> downloadSummary(Long meetingId, Long taskId, String token) {
        meetingService.getById(meetingId);
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM export_task WHERE tenant_id=? AND id=? AND biz_type='MEETING_SUMMARY' AND biz_id=? AND is_deleted=0",
                Integer.class,
                tenantId(),
                taskId,
                String.valueOf(meetingId)
        );
        if (count == null || count == 0) {
            throw new BusinessException(10003, "会议总结任务不存在");
        }
        return exportTaskService.download(taskId, token);
    }

    private Long safeUserId() {
        Long userId = AuthContext.userId();
        return userId == null ? 0L : userId;
    }

    private Map<String, Object> reconcileFailedSummaryTask(Map<String, Object> row) {
        String status = textValue(row.get("status")).toUpperCase();
        Long taskId = toLong(row.get("id"));
        if (taskId == null || taskId <= 0L || !"PENDING".equals(status)) {
            return row;
        }
        List<Map<String, Object>> jobs = jdbcTemplate.queryForList(
                "SELECT status, retry_count FROM async_job WHERE tenant_id=? AND job_type='EXPORT_TASK' AND payload=? ORDER BY id DESC LIMIT 1",
                tenantId(),
                "{\"taskId\":" + taskId + "}"
        );
        if (jobs.isEmpty()) {
            return row;
        }
        String jobStatus = textValue(jobs.get(0).get("status")).toUpperCase();
        if (!"FAILED".equals(jobStatus)) {
            return row;
        }
        String errorMessage = firstNonEmptyText(
                textValue(row.get("error_message")),
                "异步导出任务已失败，请重新生成会议总结"
        );
        jdbcTemplate.update(
                "UPDATE export_task SET status='FAILED', retry_count=max_retry, error_message=?, updated_at=CURRENT_TIMESTAMP, updated_by=? WHERE tenant_id=? AND id=? AND status='PENDING'",
                errorMessage,
                safeUserId(),
                tenantId(),
                taskId
        );
        row.put("status", "FAILED");
        row.put("retry_count", row.get("max_retry"));
        row.put("error_message", errorMessage);
        return row;
    }


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

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

    public Map<String, Object> presignMaterialUpload(Long meetingId, String moduleCode, String fileName, String contentType) {
        meetingService.getById(meetingId);
        validateModule(moduleCode);
        String name = fileName == null ? "" : fileName.trim();
        if (name.isEmpty()) {
            throw new BusinessException(10001, "文件名不能为空");
        }
        String ext = "";
        int idx = name.lastIndexOf('.');
        if (idx > 0 && idx < name.length() - 1) {
            ext = "." + name.substring(idx + 1).toLowerCase();
        }
        String normalizedType = contentType == null || contentType.trim().isEmpty() ? "application/octet-stream" : contentType.trim();
        String objectKey = "meeting/material/" + tenantId() + "/" + meetingId + "/" + moduleCode.toLowerCase() + "/" +
                UUID.randomUUID().toString().replace("-", "") + ext;
        String uploadUrl = ossService.generateUploadUrl(objectKey, normalizedType);
        Map<String, Object> result = new LinkedHashMap<String, Object>();
        result.put("objectKey", objectKey);
        result.put("uploadUrl", uploadUrl);
        result.put("contentType", normalizedType);
        result.put("method", "PUT");
        return result;
    }

    private MeetingMaterial upsert(Long meetingId, String moduleCode, String contentJson, String remark, String actionType, String status) {
        meetingService.getById(meetingId);
        validateModule(moduleCode);

        List<MeetingMaterial> existingList = jdbcTemplate.query(
                "SELECT id, meeting_id, module_code, content_json, status, " +
                        "audit_node_status, audit_aggregate_status, submit_remark, reject_count, last_reject_reason, " +
                        "DATE_FORMAT(resubmit_at, '%Y-%m-%d %H:%i:%s') AS resubmit_at, version_no, is_latest_version, " +
                        "DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at " +
                        "FROM meeting_material WHERE tenant_id=? AND meeting_id=? AND module_code=? AND is_deleted=0 LIMIT 1",
                MATERIAL_ROW_MAPPER,
                tenantId(),
                meetingId,
                moduleCode
        );
        int nextVersion = 1;
        Long materialId;
        if (existingList.isEmpty()) {
            jdbcTemplate.update(
                    "INSERT INTO meeting_material (tenant_id, meeting_id, module_code, content_json, status, audit_node_status, audit_aggregate_status, submit_remark, reject_count, version_no, is_latest_version, created_by, updated_by) " +
                            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 1, 1, 0, 0)",
                    tenantId(),
                    meetingId,
                    moduleCode,
                    contentJson,
                    status,
                    "SUBMIT".equals(actionType) ? "PENDING" : null,
                    "SUBMIT".equals(actionType) ? "PENDING" : null,
                    remark
            );
            materialId = jdbcTemplate.queryForObject(
                    "SELECT id FROM meeting_material WHERE tenant_id=? AND meeting_id=? AND module_code=? LIMIT 1",
                    Long.class,
                    tenantId(),
                    meetingId,
                    moduleCode
            );
        } else {
            MeetingMaterial existing = existingList.get(0);
            nextVersion = existing.getVersionNo() + 1;
            materialId = existing.getId();
            jdbcTemplate.update(
                    "UPDATE meeting_material SET content_json=?, status=?, audit_node_status=?, audit_aggregate_status=?, submit_remark=?, " +
                            "version_no=?, is_latest_version=1, updated_at=CURRENT_TIMESTAMP, updated_by=0 WHERE id=?",
                    contentJson,
                    status,
                    "SUBMIT".equals(actionType) ? "PENDING" : existing.getAuditNodeStatus(),
                    "SUBMIT".equals(actionType) ? "PENDING" : existing.getAuditAggregateStatus(),
                    remark,
                    nextVersion,
                    materialId
            );
        }

        jdbcTemplate.update(
                "INSERT INTO meeting_material_history (tenant_id, meeting_id, module_code, version_no, action_type, content_json, remark, created_by) " +
                        "VALUES (?, ?, ?, ?, ?, ?, ?, 0)",
                tenantId(),
                meetingId,
                moduleCode,
                nextVersion,
                actionType,
                contentJson,
                remark
        );

        if ("EXPERT_LIST".equals(moduleCode)) {
            syncInvoiceStructuredData(meetingId, materialId, contentJson);
        }

        List<MeetingMaterial> result = jdbcTemplate.query(
                "SELECT id, meeting_id, module_code, content_json, status, " +
                        "audit_node_status, audit_aggregate_status, submit_remark, reject_count, last_reject_reason, " +
                        "DATE_FORMAT(resubmit_at, '%Y-%m-%d %H:%i:%s') AS resubmit_at, version_no, is_latest_version, " +
                        "DATE_FORMAT(updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at " +
                        "FROM meeting_material WHERE id=?",
                MATERIAL_ROW_MAPPER,
                materialId
        );
        if (result.isEmpty()) {
            throw new BusinessException(10003, "资料不存在");
        }
        return result.get(0);
    }

    private void validateModule(String moduleCode) {
        if (moduleCode == null || !SUPPORTED_MODULES.contains(moduleCode)) {
            throw new BusinessException(10001, "不支持的资料模块");
        }
    }

    private String resolveStorageModuleCode(String moduleCode) {
        if ("EXPERT_PROFILE".equals(moduleCode)) {
            return "WRITE_OFF_DOCS";
        }
        return moduleCode;
    }

    private void validateSubmitContent(String moduleCode, String contentJson) {
        try {
            Map<String, Object> map = objectMapper.readValue(contentJson, new TypeReference<Map<String, Object>>() {});
            if ("BASIC_INFO".equals(moduleCode)) {
                requireField(map, "chairmanExpertIds");
                requireField(map, "speakerExpertIds");
                requireField(map, "hostExpertIds");
                requireField(map, "discussionGuestExpertIds");
                requireField(map, "guestCount");
                requireField(map, "attendeeCount");
                requireField(map, "attendeeActualCount");
                requireField(map, "targetAudience");
                requireField(map, "mainAgenda");
                requireField(map, "improvementSuggestion");
                requireField(map, "meetingEffect");
                requireIdList(map.get("chairmanExpertIds"), "chairmanExpertIds");
                requireIdList(map.get("speakerExpertIds"), "speakerExpertIds");
                requireIdList(map.get("hostExpertIds"), "hostExpertIds");
                requireIdList(map.get("discussionGuestExpertIds"), "discussionGuestExpertIds");
            } else if ("WRITE_OFF_DOCS".equals(moduleCode)) {
                requireField(map, "agenda");
                requireField(map, "signInSheet");
                requireField(map, "invitation");
                requireAgendaField(map.get("agenda"));
                requireNestedField(map, "signInSheet", "ossKey");
                requireOptionalNestedField(map, "themePhoto", "ossKey");
                requireInvitationList(map.get("invitation"));
            } else if ("EXPERT_LIST".equals(moduleCode)) {
                requireField(map, "onsitePhoto");
                requireField(map, "laborProtocol");
                requireField(map, "invoiceDetail");
                Map<String, Object> onsitePhoto = asObjectMap(map.get("onsitePhoto"), "onsitePhoto");
                Map<String, Object> laborProtocol = asObjectMap(map.get("laborProtocol"), "laborProtocol");
                Map<String, Object> invoiceDetail = asObjectMap(map.get("invoiceDetail"), "invoiceDetail");
                requireField(onsitePhoto, "photos");
                requirePhotoList(onsitePhoto.get("photos"));
                requireField(laborProtocol, "details");
                requireLaborDetailList(laborProtocol.get("details"));
                requireField(invoiceDetail, "invoices");
                requireInvoiceList(invoiceDetail.get("invoices"));
            } else if ("MEETING_INVOICE".equals(moduleCode)) {
                if (map.get("sections") instanceof Collection) {
                    requireMeetingInvoiceSections(map.get("sections"));
                } else {
                    requireField(map, "attachments");
                    requireMeetingInvoiceAttachmentList(map.get("attachments"));
                }
            }
        } catch (Exception e) {
            throw new BusinessException(10001, "资料内容格式错误或缺少必填字段");
        }
    }

    private List<Map<String, Object>> buildReviewItems(String moduleCode, String contentJson) {
        List<Map<String, Object>> items = new ArrayList<>();
        Map<String, Object> root;
        try {
            root = objectMapper.readValue(contentJson, new TypeReference<Map<String, Object>>() {});
        } catch (Exception e) {
            return items;
        }
        if ("BASIC_INFO".equals(moduleCode)) {
            addReviewItem(items, "discussionGuestExpertIds", "讨论嘉宾");
            addReviewItem(items, "chairmanExpertIds", "大会主席");
            addReviewItem(items, "speakerExpertIds", "会议讲者");
            addReviewItem(items, "hostExpertIds", "会议主持");
            addReviewItem(items, "guestCount", "嘉宾人数");
            addReviewItem(items, "attendeeCount", "计划参会人数");
            addReviewItem(items, "attendeeActualCount", "实到人数");
            addReviewItem(items, "targetAudience", "主要参会对象");
            addReviewItem(items, "mainAgenda", "主要议程");
            addReviewItem(items, "improvementSuggestion", "不足/改进建议");
            addReviewItem(items, "meetingEffect", "会议效果");
        } else if ("WRITE_OFF_DOCS".equals(moduleCode)) {
            Object agendaObj = root.get("agenda");
            if (agendaObj instanceof Collection) {
                int idx = 1;
                for (Object ignored : (Collection<?>) agendaObj) {
                    addReviewItem(items, "agenda:" + idx, "会议日程#" + idx);
                    idx++;
                }
            } else {
                addReviewItem(items, "agenda", "会议日程");
            }
            addReviewItem(items, "signInSheet", "签到表");
            Map<String, Object> themePhoto = asObjectMapOrEmpty(root.get("themePhoto"));
            if (!stringValue(themePhoto.get("ossKey")).isEmpty()) {
                addReviewItem(items, "themePhoto", "主题照片");
            }
            Object invitationObj = root.get("invitation");
            if (invitationObj instanceof Collection) {
                int idx = 1;
                for (Object invitationItem : (Collection<?>) invitationObj) {
                    if (invitationItem == null) {
                        // keep index continuity for malformed rows
                    }
                    addReviewItem(items, "invitation:" + idx, "邀请函/通知#" + idx);
                    idx++;
                }
            }
        } else if ("EXPERT_PROFILE".equals(moduleCode)) {
            Map<String, Object> profileFile = asObjectMapOrEmpty(root.get("profileFile"));
            String ossKey = stringValue(firstNonNull(profileFile.get("ossKey"), root.get("ossKey")));
            if (ossKey != null && !ossKey.isEmpty()) {
                addReviewItem(items, "expert_profile_file", "涓撳绠€浠?涓插満鏂囦欢");
            }
        } else if ("EXPERT_LIST".equals(moduleCode)) {
            Map<String, Object> onsiteRoot = asObjectMapOrEmpty(root.get("onsitePhoto"));
            Map<String, Object> laborRoot = asObjectMapOrEmpty(root.get("laborProtocol"));
            Map<String, Object> invoiceRoot = asObjectMapOrEmpty(root.get("invoiceDetail"));
            Object photosObj = onsiteRoot.get("photos");
            if (photosObj instanceof Collection) {
                int idx = 1;
                for (Object one : (Collection<?>) photosObj) {
                    if (one instanceof Map) {
                        String ossKey = stringValue(((Map<?, ?>) one).get("ossKey"));
                        addReviewItem(items, "photo:" + (ossKey == null || ossKey.isEmpty() ? idx : ossKey), "现场照片#" + idx);
                    } else {
                        addReviewItem(items, "photo:" + idx, "现场照片#" + idx);
                    }
                    idx++;
                }
            }
            Object expertSummariesObj = onsiteRoot.get("expertSummaries");
            if (expertSummariesObj instanceof Collection) {
                for (Object one : (Collection<?>) expertSummariesObj) {
                    if (one instanceof Map) {
                        Map<?, ?> summaryMap = (Map<?, ?>) one;
                        String expertId = stringValue(summaryMap.get("expertId"));
                        String expertName = stringValue(summaryMap.get("expertName"));
                        if (expertId != null && !expertId.isEmpty()) {
                            String key = "onsite_summary:" + expertId;
                            String label = "现场说明-" + (expertName == null || expertName.isEmpty() ? "讲者" : expertName);
                            addReviewItem(items, key, label);
                        }
                    }
                }
            } else {
                addReviewItem(items, "onsite_summary", "现场说明");
            }
            Object detailsObj = laborRoot.get("details");
            if (detailsObj instanceof Collection) {
                int idx = 1;
                for (Object one : (Collection<?>) detailsObj) {
                    if (one instanceof Map) {
                        Map<?, ?> detail = (Map<?, ?>) one;
                        String expertId = stringValue(detail.get("expertId"));
                        String expertName = stringValue(detail.get("expertName"));
                        String key;
                        if (expertId == null || expertId.isEmpty()) {
                            key = "labor:" + idx;
                        } else {
                            key = "labor:" + expertId;
                        }
                        String label = "劳务协议-" + (expertName == null || expertName.isEmpty() ? ("条目#" + idx) : expertName);
                        addReviewItem(items, key, label);
                    } else {
                        addReviewItem(items, "labor:" + idx, "劳务协议-条目#" + idx);
                    }
                    idx++;
                }
            }
            Object invoicesObj = invoiceRoot.get("invoices");
            if (invoicesObj instanceof Collection) {
                int idx = 1;
                for (Object one : (Collection<?>) invoicesObj) {
                    if (one instanceof Map) {
                        Map<?, ?> invoice = (Map<?, ?>) one;
                        String invoiceNo = stringValue(invoice.get("invoiceNo"));
                        String expertName = stringValue(invoice.get("expertName"));
                        String key = "invoice:" + (invoiceNo == null || invoiceNo.isEmpty() ? idx : invoiceNo);
                        String label = "发票明细-" +
                                (expertName == null || expertName.isEmpty() ? "条目" : expertName) +
                                "#" + (invoiceNo == null || invoiceNo.isEmpty() ? idx : invoiceNo);
                        addReviewItem(items, key, label);
                    } else {
                        addReviewItem(items, "invoice:" + idx, "发票明细-条目#" + idx);
                    }
                    idx++;
                }
            }
            addReviewItem(items, "invoice_summary", "发票明细汇总说明");
        } else if ("MEETING_INVOICE".equals(moduleCode) && root.get("sections") instanceof Collection) {
            Object sectionsObj = root.get("sections");
            for (Object one : (Collection<?>) sectionsObj) {
                if (!(one instanceof Map)) {
                    continue;
                }
                Map<String, Object> section = asObjectMapOrEmpty(one);
                String sectionCode = stringValue(firstNonNull(section.get("sectionCode"), section.get("code")));
                if (sectionCode == null || sectionCode.isEmpty() || !MEETING_INVOICE_SECTION_CODES.contains(sectionCode)) {
                    continue;
                }
                List<String> fieldKeys = MEETING_INVOICE_SECTION_FIELD_KEYS.get(sectionCode);
                if (fieldKeys != null) {
                    for (String fieldKey : fieldKeys) {
                        if (hasNonEmptyMeetingInvoiceFieldFile(section, fieldKey)) {
                            addReviewItem(items, buildMeetingInvoiceFieldReviewKey(sectionCode, fieldKey), buildMeetingInvoiceFieldReviewKey(sectionCode, fieldKey));
                        }
                    }
                }
                if (MEETING_INVOICE_AMOUNT_SECTION_CODES.contains(sectionCode)) {
                    Object amountObj = firstNonNull(section.get("totalAmountCent"), section.get("totalAmountYuan"));
                    if (amountObj != null && !String.valueOf(amountObj).trim().isEmpty()) {
                        addReviewItem(items, buildMeetingInvoiceAmountReviewKey(sectionCode), buildMeetingInvoiceAmountReviewKey(sectionCode));
                    }
                }
            }
        } else if ("MEETING_INVOICE".equals(moduleCode)) {
            Object attachmentsObj = root.get("attachments");
            if (attachmentsObj instanceof Collection) {
                int idx = 1;
                for (Object one : (Collection<?>) attachmentsObj) {
                    if (one instanceof Map) {
                        Map<?, ?> item = (Map<?, ?>) one;
                        String attachmentName = stringValue(item.get("attachmentName"));
                        String ossKey = stringValue(item.get("ossKey"));
                        String key = "meeting_invoice_file:" + (ossKey == null || ossKey.isEmpty() ? idx : ossKey);
                        String label = "会议发票附件-" + (attachmentName == null || attachmentName.isEmpty() ? ("条目#" + idx) : attachmentName);
                        addReviewItem(items, key, label);
                    } else {
                        addReviewItem(items, "meeting_invoice_file:" + idx, "会议发票附件-条目#" + idx);
                    }
                    idx++;
                }
            }
            addReviewItem(items, "meeting_invoice_summary", "会议发票说明");
        }
        return items;
    }

    private void addReviewItem(List<Map<String, Object>> items, String itemKey, String itemLabel) {
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("itemKey", itemKey);
        row.put("itemLabel", itemLabel);
        items.add(row);
    }

    private String buildMeetingInvoiceFieldReviewKey(String sectionCode, String fieldKey) {
        return "meeting_invoice:" + sectionCode + ":" + fieldKey;
    }

    private String buildMeetingInvoiceAmountReviewKey(String sectionCode) {
        return "meeting_invoice:" + sectionCode + ":amount";
    }

    private boolean hasNonEmptyMeetingInvoiceFieldFile(Map<String, Object> section, String fieldKey) {
        if (section == null || fieldKey == null || fieldKey.trim().isEmpty()) {
            return false;
        }
        Object filesObj = section.get("files");
        if (filesObj instanceof Collection) {
            for (Object fileObj : (Collection<?>) filesObj) {
                if (!(fileObj instanceof Map)) {
                    continue;
                }
                Map<?, ?> fileMap = (Map<?, ?>) fileObj;
                String currentFieldKey = stringValue(firstNonNull(fileMap.get("fieldKey"), fileMap.get("key")));
                String ossKey = stringValue(firstNonNull(fileMap.get("ossKey"), fileMap.get("objectKey")));
                if (fieldKey.equals(currentFieldKey) && ossKey != null && !ossKey.isEmpty()) {
                    return true;
                }
            }
        }
        Object legacyObj = section.get(fieldKey);
        if (legacyObj instanceof Collection) {
            for (Object fileObj : (Collection<?>) legacyObj) {
                if (!(fileObj instanceof Map)) {
                    continue;
                }
                String ossKey = stringValue(firstNonNull(((Map<?, ?>) fileObj).get("ossKey"), ((Map<?, ?>) fileObj).get("objectKey")));
                if (ossKey != null && !ossKey.isEmpty()) {
                    return true;
                }
            }
            return false;
        }
        if (legacyObj instanceof Map) {
            String ossKey = stringValue(firstNonNull(((Map<?, ?>) legacyObj).get("ossKey"), ((Map<?, ?>) legacyObj).get("objectKey")));
            return ossKey != null && !ossKey.isEmpty();
        }
        return false;
    }

    private void requireField(Map<String, Object> map, String key) {
        Object value = map.get(key);
        if (value == null || String.valueOf(value).trim().isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: " + key);
        }
    }

    private void requireNestedField(Map<String, Object> map, String key, String nestedKey) {
        Object target = map.get(key);
        if (!(target instanceof Map)) {
            throw new BusinessException(10001, "提交失败，字段格式错误: " + key);
        }
        Map<?, ?> nested = (Map<?, ?>) target;
        Object nestedValue = nested.get(nestedKey);
        if (nestedValue == null || String.valueOf(nestedValue).trim().isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: " + key + "." + nestedKey);
        }
    }

    private void requireOptionalNestedField(Map<String, Object> map, String key, String nestedKey) {
        Object target = map.get(key);
        if (target == null) {
            return;
        }
        if (!(target instanceof Map)) {
            throw new BusinessException(10001, "invalid nested field " + key);
        }
        Map<?, ?> nested = (Map<?, ?>) target;
        Object nestedValue = nested.get(nestedKey);
        if (nestedValue == null || String.valueOf(nestedValue).trim().isEmpty()) {
            return;
        }
    }

    private void requireAgendaField(Object value) {
        if (value instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) value;
            Object ossKey = map.get("ossKey");
            if (ossKey == null || String.valueOf(ossKey).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: agenda.ossKey");
            }
            return;
        }
        if (value instanceof Collection) {
            Collection<?> list = (Collection<?>) value;
            if (list.isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: agenda");
            }
            for (Object item : list) {
                if (!(item instanceof Map)) {
                    throw new BusinessException(10001, "提交失败，字段格式错误: agenda");
                }
                Map<?, ?> row = (Map<?, ?>) item;
                Object ossKey = row.get("ossKey");
                if (ossKey == null || String.valueOf(ossKey).trim().isEmpty()) {
                    throw new BusinessException(10001, "提交失败，缺少必填字段: agenda.ossKey");
                }
            }
            return;
        }
        throw new BusinessException(10001, "提交失败，字段格式错误: agenda");
    }

    private Map<String, Object> asObjectMap(Object value, String fieldName) {
        if (!(value instanceof Map)) {
            throw new BusinessException(10001, "提交失败，字段格式错误: " + fieldName);
        }
        @SuppressWarnings("unchecked")
        Map<String, Object> target = (Map<String, Object>) value;
        return target;
    }

    private Map<String, Object> asObjectMapOrEmpty(Object value) {
        if (!(value instanceof Map)) {
            return new LinkedHashMap<>();
        }
        @SuppressWarnings("unchecked")
        Map<String, Object> target = (Map<String, Object>) value;
        return target;
    }

    private void requireInvitationList(Object value) {
        if (!(value instanceof Collection)) {
            throw new BusinessException(10001, "提交失败，字段格式错误: invitation");
        }
        Collection<?> list = (Collection<?>) value;
        if (list.isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: invitation");
        }
        for (Object item : list) {
            if (!(item instanceof Map)) {
                throw new BusinessException(10001, "提交失败，字段格式错误: invitation");
            }
            Map<?, ?> invitation = (Map<?, ?>) item;
            Object ossKey = invitation.get("ossKey");
            if (ossKey == null || String.valueOf(ossKey).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: invitation.ossKey");
            }
        }
    }

    private void requirePhotoList(Object value) {
        if (!(value instanceof Collection)) {
            throw new BusinessException(10001, "提交失败，字段格式错误: photos");
        }
        Collection<?> list = (Collection<?>) value;
        if (list.isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: photos");
        }
        for (Object item : list) {
            if (!(item instanceof Map)) {
                throw new BusinessException(10001, "提交失败，字段格式错误: photos");
            }
            Map<?, ?> photo = (Map<?, ?>) item;
            Object ossKey = photo.get("ossKey");
            if (ossKey == null || String.valueOf(ossKey).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: photos.ossKey");
            }
        }
    }

    private void requireLaborDetailList(Object value) {
        if (!(value instanceof Collection)) {
            throw new BusinessException(10001, "提交失败，字段格式错误: details");
        }
        Collection<?> list = (Collection<?>) value;
        if (list.isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: details");
        }
        for (Object item : list) {
            if (!(item instanceof Map)) {
                throw new BusinessException(10001, "提交失败，字段格式错误: details");
            }
            Map<?, ?> detail = (Map<?, ?>) item;
            Object expertName = detail.get("expertName");
            Object amountCent = detail.get("amountCent");
            if (expertName == null || String.valueOf(expertName).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: details.expertName");
            }
            if (amountCent == null || String.valueOf(amountCent).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: details.amountCent");
            }
        }
    }

    private void requireInvoiceList(Object value) {
        if (!(value instanceof Collection)) {
            throw new BusinessException(10001, "提交失败，字段格式错误: invoices");
        }
        Collection<?> list = (Collection<?>) value;
        if (list.isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: invoices");
        }
        for (Object item : list) {
            if (!(item instanceof Map)) {
                throw new BusinessException(10001, "提交失败，字段格式错误: invoices");
            }
            Map<?, ?> invoice = (Map<?, ?>) item;
            Object expenseType = invoice.get("expenseType");
            Object invoiceNo = invoice.get("invoiceNo");
            Object amountCent = firstNonNull(invoice.get("invoiceAmountCent"), invoice.get("amountCent"));
            Object taxCent = firstNonNull(invoice.get("taxAmountCent"), invoice.get("taxCent"));
            Object detailCent = firstNonNull(invoice.get("detailAmountCent"), invoice.get("amountCent"));
            Object files = firstNonNull(invoice.get("files"), invoice.get("file"));
            if (expenseType == null || String.valueOf(expenseType).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: invoices.expenseType");
            }
            if (invoiceNo == null || String.valueOf(invoiceNo).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: invoices.invoiceNo");
            }
            if (amountCent == null || String.valueOf(amountCent).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: invoices.amountCent");
            }
            if (taxCent == null || String.valueOf(taxCent).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: invoices.taxCent");
            }
            if (detailCent == null || String.valueOf(detailCent).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: invoices.detailAmountCent");
            }

            long invoiceAmount = safeLong(amountCent, "invoices.invoiceAmountCent");
            long taxAmount = safeLong(taxCent, "invoices.taxAmountCent");
            long detailAmount = safeLong(detailCent, "invoices.detailAmountCent");
            if (invoiceAmount < 0 || taxAmount < 0 || detailAmount < 0) {
                throw new BusinessException(10001, "提交失败，金额字段必须大于等于0");
            }

            if (files instanceof Collection) {
                requireInvoiceFileList((Collection<?>) files);
            } else if (files instanceof Map) {
                requireSingleInvoiceFile((Map<?, ?>) files, "invoices.file");
            } else {
                throw new BusinessException(10001, "提交失败，字段格式错误: invoices.files");
            }
        }
    }

    private void requireInvoiceFileList(Collection<?> files) {
        if (files.isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: invoices.files");
        }
        for (Object file : files) {
            if (!(file instanceof Map)) {
                throw new BusinessException(10001, "提交失败，字段格式错误: invoices.files");
            }
            requireSingleInvoiceFile((Map<?, ?>) file, "invoices.files");
        }
    }

    private void requireMeetingInvoiceAttachmentList(Object value) {
        if (!(value instanceof Collection)) {
            throw new BusinessException(10001, "提交失败，字段格式错误: attachments");
        }
        Collection<?> list = (Collection<?>) value;
        if (list.isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: attachments");
        }
        for (Object item : list) {
            if (!(item instanceof Map)) {
                throw new BusinessException(10001, "提交失败，字段格式错误: attachments");
            }
            Map<?, ?> file = (Map<?, ?>) item;
            Object attachmentName = file.get("attachmentName");
            Object ossKey = file.get("ossKey");
            if (attachmentName == null || String.valueOf(attachmentName).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: attachments.attachmentName");
            }
            if (ossKey == null || String.valueOf(ossKey).trim().isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: attachments.ossKey");
            }
        }
    }

    private void requireSingleInvoiceFile(Map<?, ?> file, String path) {
        Object fileType = file.get("fileType");
        Object fileName = file.get("fileName");
        Object ossKey = file.get("ossKey");
        if (fileType == null || String.valueOf(fileType).trim().isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: " + path + ".fileType");
        }
        if (fileName == null || String.valueOf(fileName).trim().isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: " + path + ".fileName");
        }
        if (ossKey == null || String.valueOf(ossKey).trim().isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: " + path + ".ossKey");
        }
    }

    private void requireMeetingInvoiceSections(Object value) {
        if (!(value instanceof Collection)) {
            throw new BusinessException(10001, "提交失败，字段格式错误: sections");
        }
        Collection<?> sections = (Collection<?>) value;
        if (sections.isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: sections");
        }
        for (Object sectionObj : sections) {
            if (!(sectionObj instanceof Map)) {
                throw new BusinessException(10001, "提交失败，字段格式错误: sections");
            }
            Map<?, ?> section = (Map<?, ?>) sectionObj;
            String sectionCode = stringValue(firstNonNull(section.get("sectionCode"), section.get("code")));
            if (sectionCode == null || sectionCode.isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: sections.sectionCode");
            }
            Object filesObj = section.get("files");
            if (!(filesObj instanceof Collection)) {
                throw new BusinessException(10001, "提交失败，字段格式错误: sections.files");
            }
            Collection<?> files = (Collection<?>) filesObj;
            if (!"OTHER".equals(sectionCode) && files.isEmpty()) {
                throw new BusinessException(10001, "提交失败，缺少必填字段: sections.files");
            }
            for (Object fileObj : files) {
                if (!(fileObj instanceof Map)) {
                    throw new BusinessException(10001, "提交失败，字段格式错误: sections.files");
                }
                Map<?, ?> fileMap = (Map<?, ?>) fileObj;
                Object ossKey = fileMap.get("ossKey");
                if (ossKey == null || String.valueOf(ossKey).trim().isEmpty()) {
                    throw new BusinessException(10001, "提交失败，缺少必填字段: sections.files.ossKey");
                }
            }
            if (MEETING_INVOICE_AMOUNT_SECTION_CODES.contains(sectionCode)) {
                Object amountObj = firstNonNull(section.get("totalAmountCent"), section.get("totalAmountYuan"));
                if (amountObj == null || String.valueOf(amountObj).trim().isEmpty()) {
                    throw new BusinessException(10001, "提交失败，缺少必填字段: sections.totalAmount");
                }
            }
        }
    }

    private void validateMeetingBudgetLimit(Long meetingId, String moduleCode, String currentContentJson) {
        validateModule(moduleCode);
        Meeting meeting = meetingService.getById(meetingId);
        long budgetCent = Math.max(0L, meeting.getBudgetCent());
        String expertListContentJson = "EXPERT_LIST".equals(moduleCode)
                ? currentContentJson
                : getMaterialContentJsonOrEmpty(meetingId, "EXPERT_LIST");
        String meetingInvoiceContentJson = "MEETING_INVOICE".equals(moduleCode)
                ? currentContentJson
                : getMaterialContentJsonOrEmpty(meetingId, "MEETING_INVOICE");
        long laborTotalCent = extractLaborTotalCent(expertListContentJson);
        long expertInvoiceTotalCent = extractExpertInvoiceTotalCent(expertListContentJson);
        long meetingInvoiceTotalCent = extractMeetingInvoiceTotalCent(meetingInvoiceContentJson);
        long usedTotalCent = laborTotalCent + expertInvoiceTotalCent + meetingInvoiceTotalCent;
        if (usedTotalCent <= budgetCent) {
            return;
        }
        long overCent = usedTotalCent - budgetCent;
        throw new BusinessException(10001, "预算校验不通过：当前会议预算" + formatYuan(budgetCent)
                + "元，已用" + formatYuan(usedTotalCent) + "元，超预算" + formatYuan(overCent) + "元");
    }

    private String getMaterialContentJsonOrEmpty(Long meetingId, String moduleCode) {
        List<String> rows = jdbcTemplate.query(
                "SELECT content_json FROM meeting_material WHERE tenant_id=? AND meeting_id=? AND module_code=? AND is_deleted=0 LIMIT 1",
                (rs, n) -> rs.getString("content_json"),
                tenantId(),
                meetingId,
                moduleCode
        );
        if (rows.isEmpty()) {
            return "";
        }
        return rows.get(0) == null ? "" : rows.get(0);
    }

    private long extractLaborTotalCent(String contentJson) {
        if (contentJson == null || contentJson.trim().isEmpty()) {
            return 0L;
        }
        try {
            Map<String, Object> root = objectMapper.readValue(contentJson, new TypeReference<Map<String, Object>>() {});
            Map<String, Object> laborProtocol = asObjectMapOrEmpty(root.get("laborProtocol"));
            Object detailObj = laborProtocol.get("details");
            if (!(detailObj instanceof Collection)) {
                return 0L;
            }
            long total = 0L;
            for (Object one : (Collection<?>) detailObj) {
                if (!(one instanceof Map)) {
                    continue;
                }
                Map<?, ?> row = (Map<?, ?>) one;
                total += Math.max(0L, parseCentValue(row.get("amountCent")));
            }
            return total;
        } catch (Exception e) {
            return 0L;
        }
    }

    private long extractExpertInvoiceTotalCent(String contentJson) {
        if (contentJson == null || contentJson.trim().isEmpty()) {
            return 0L;
        }
        try {
            Map<String, Object> root = objectMapper.readValue(contentJson, new TypeReference<Map<String, Object>>() {});
            Map<String, Object> invoiceDetail = asObjectMapOrEmpty(root.get("invoiceDetail"));
            Object invoiceObj = invoiceDetail.get("invoices");
            if (!(invoiceObj instanceof Collection)) {
                return 0L;
            }
            long total = 0L;
            for (Object one : (Collection<?>) invoiceObj) {
                if (!(one instanceof Map)) {
                    continue;
                }
                Map<?, ?> row = (Map<?, ?>) one;
                Object amountObj = firstNonNull(row.get("invoiceAmountCent"), row.get("amountCent"));
                total += Math.max(0L, parseCentValue(amountObj));
            }
            return total;
        } catch (Exception e) {
            return 0L;
        }
    }

    private long extractMeetingInvoiceTotalCent(String contentJson) {
        if (contentJson == null || contentJson.trim().isEmpty()) {
            return 0L;
        }
        try {
            Map<String, Object> root = objectMapper.readValue(contentJson, new TypeReference<Map<String, Object>>() {});
            Object sectionsObj = root.get("sections");
            if (!(sectionsObj instanceof Collection)) {
                return 0L;
            }
            long total = 0L;
            for (Object one : (Collection<?>) sectionsObj) {
                if (!(one instanceof Map)) {
                    continue;
                }
                Map<?, ?> section = (Map<?, ?>) one;
                String sectionCode = stringValue(firstNonNull(section.get("sectionCode"), section.get("code")));
                if (sectionCode == null || !MEETING_INVOICE_AMOUNT_SECTION_CODES.contains(sectionCode)) {
                    continue;
                }
                Object centObj = firstNonNull(section.get("totalAmountCent"), section.get("totalFeeCent"));
                if (centObj != null && !String.valueOf(centObj).trim().isEmpty()) {
                    total += Math.max(0L, parseCentValue(centObj));
                    continue;
                }
                Object yuanObj = firstNonNull(section.get("totalAmountYuan"), section.get("totalFeeYuan"));
                total += Math.max(0L, parseYuanToCent(yuanObj));
            }
            return total;
        } catch (Exception e) {
            return 0L;
        }
    }

    private long parseCentValue(Object value) {
        if (value == null || String.valueOf(value).trim().isEmpty()) {
            return 0L;
        }
        try {
            BigDecimal decimal = new BigDecimal(String.valueOf(value).trim());
            return decimal.setScale(0, RoundingMode.HALF_UP).longValue();
        } catch (Exception e) {
            throw new BusinessException(10001, "提交失败，金额字段不是合法数字");
        }
    }

    private long parseYuanToCent(Object value) {
        if (value == null || String.valueOf(value).trim().isEmpty()) {
            return 0L;
        }
        try {
            BigDecimal decimal = new BigDecimal(String.valueOf(value).trim());
            return decimal.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).longValue();
        } catch (Exception e) {
            throw new BusinessException(10001, "提交失败，金额字段不是合法数字");
        }
    }

    private String formatYuan(long cent) {
        BigDecimal yuan = new BigDecimal(cent).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
        return yuan.toPlainString();
    }

    private void requireIdList(Object value, String key) {
        if (!(value instanceof Collection)) {
            throw new BusinessException(10001, "提交失败，字段格式错误: " + key);
        }
        Collection<?> list = (Collection<?>) value;
        if (list.isEmpty()) {
            throw new BusinessException(10001, "提交失败，缺少必填字段: " + key);
        }
        for (Object item : list) {
            if (!(item instanceof Number) && !String.valueOf(item).matches("^\\d+$")) {
                throw new BusinessException(10001, "提交失败，字段格式错误: " + key);
            }
        }
    }

    private Long tenantId() {
        return AuthContext.requireTenantId();
    }

    private void syncInvoiceStructuredData(Long meetingId, Long materialId, String contentJson) {
        try {
            Map<String, Object> root = objectMapper.readValue(contentJson, new TypeReference<Map<String, Object>>() {});
            Map<String, Object> invoiceDetail = asObjectMapOrEmpty(root.get("invoiceDetail"));
            Object invoiceNode = firstNonNull(invoiceDetail.get("invoices"), invoiceDetail.get("items"));
            if (!(invoiceNode instanceof Collection)) {
                return;
            }

            Collection<?> invoices = (Collection<?>) invoiceNode;
            jdbcTemplate.update("DELETE FROM meeting_material_invoice_file WHERE tenant_id=? AND meeting_id=?", tenantId(), meetingId);
            jdbcTemplate.update("DELETE FROM meeting_material_invoice_item WHERE tenant_id=? AND meeting_id=?", tenantId(), meetingId);

            Map<String, Long> categoryAmount = new LinkedHashMap<>();
            long total = 0L;
            Meeting meeting = meetingService.getById(meetingId);

            for (Object obj : invoices) {
                if (!(obj instanceof Map)) {
                    continue;
                }
                Map<?, ?> invoice = (Map<?, ?>) obj;
                String expenseType = stringValue(invoice.get("expenseType"));
                String invoiceNo = stringValue(invoice.get("invoiceNo"));
                long invoiceAmountCent = safeLong(firstNonNull(invoice.get("invoiceAmountCent"), invoice.get("amountCent")), "invoiceAmountCent");
                long taxAmountCent = safeLong(firstNonNull(invoice.get("taxAmountCent"), invoice.get("taxCent")), "taxAmountCent");
                long detailAmountCent = safeLong(firstNonNull(invoice.get("detailAmountCent"), invoice.get("amountCent")), "detailAmountCent");
                String vendorName = stringValue(invoice.get("vendorName"));
                String occurDate = stringValue(invoice.get("occurDate"));
                String itemRemark = stringValue(invoice.get("remark"));

                jdbcTemplate.update(
                        "INSERT INTO meeting_material_invoice_item (tenant_id, meeting_id, material_id, expense_type, invoice_no, invoice_amount_cent, tax_amount_cent, detail_amount_cent, vendor_name, occur_date, remark, created_by, updated_by) " +
                                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, STR_TO_DATE(?, '%Y-%m-%d'), ?, 0, 0)",
                        tenantId(),
                        meetingId,
                        materialId,
                        expenseType,
                        invoiceNo,
                        invoiceAmountCent,
                        taxAmountCent,
                        detailAmountCent,
                        vendorName,
                        occurDate,
                        itemRemark
                );
                Long invoiceItemId = jdbcTemplate.queryForObject(
                        "SELECT LAST_INSERT_ID()",
                        Long.class
                );

                Object fileNode = firstNonNull(invoice.get("files"), invoice.get("file"));
                if (fileNode instanceof Collection) {
                    for (Object fileObj : (Collection<?>) fileNode) {
                        if (fileObj instanceof Map) {
                            insertInvoiceFile(meetingId, invoiceItemId, (Map<?, ?>) fileObj);
                        }
                    }
                } else if (fileNode instanceof Map) {
                    insertInvoiceFile(meetingId, invoiceItemId, (Map<?, ?>) fileNode);
                }

                categoryAmount.put(expenseType, categoryAmount.getOrDefault(expenseType, 0L) + detailAmountCent);
                total += detailAmountCent;
            }

            String categoryAmountJson = objectMapper.writeValueAsString(categoryAmount);
            int isOverBudget = total > meeting.getBudgetCent() ? 1 : 0;

            jdbcTemplate.update(
                    "INSERT INTO meeting_invoice_summary (tenant_id, meeting_id, category_amount_cent_json, meeting_total_amount_cent, is_over_budget, created_by, updated_by) " +
                            "VALUES (?, ?, ?, ?, ?, 0, 0) " +
                            "ON DUPLICATE KEY UPDATE category_amount_cent_json=VALUES(category_amount_cent_json), meeting_total_amount_cent=VALUES(meeting_total_amount_cent), " +
                            "is_over_budget=VALUES(is_over_budget), updated_at=CURRENT_TIMESTAMP, updated_by=0",
                    tenantId(),
                    meetingId,
                    categoryAmountJson,
                    total,
                    isOverBudget
            );
        } catch (BusinessException e) {
            throw e;
        } catch (Exception e) {
            throw new BusinessException(10001, "发票结构化数据处理失败");
        }
    }

    private void insertInvoiceFile(Long meetingId, Long invoiceItemId, Map<?, ?> file) {
        String fileType = stringValue(file.get("fileType"));
        String fileName = stringValue(file.get("fileName"));
        String ossKey = stringValue(file.get("ossKey"));
        String contentType = stringValue(file.get("contentType"));
        long size = safeLong(file.get("size"), "files.size");
        if (size < 0) {
            throw new BusinessException(10001, "提交失败，文件大小必须大于等于0");
        }
        jdbcTemplate.update(
                "INSERT INTO meeting_material_invoice_file (tenant_id, meeting_id, invoice_item_id, file_type, file_name, oss_key, content_type, size, created_by) " +
                        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)",
                tenantId(),
                meetingId,
                invoiceItemId,
                fileType,
                fileName,
                ossKey,
                contentType,
                size
        );
    }

    private Object firstNonNull(Object first, Object second) {
        return first != null ? first : second;
    }


    private long safeLong(Object value, String fieldName) {
        if (value == null || String.valueOf(value).trim().isEmpty()) {
            return 0L;
        }
        try {
            return Long.parseLong(String.valueOf(value).trim());
        } catch (Exception e) {
            throw new BusinessException(10001, "提交失败，字段不是合法数字: " + fieldName);
        }
    }
}
