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.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
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;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class MeetingMaterialExportService {
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final Pattern ILLEGAL_FILE_CHARS = Pattern.compile("[\\\\/:*?\"<>|]");
    private static final List<ModuleSpec> MODULE_SPECS = Arrays.asList(
            new ModuleSpec("BASIC_INFO", "01_会议基本信息模块", "basic-info.json"),
            new ModuleSpec("WRITE_OFF_DOCS", "02_核销材料模块", "write-off-docs.json"),
            new ModuleSpec("EXPERT_LIST", "03_专家列表模块", "expert-list.json"),
            new ModuleSpec("MEETING_INVOICE", "04_会议发票模块", "meeting-invoice.json")
    );
    private static final Map<String, String> INVOICE_SECTION_TITLES = new LinkedHashMap<String, String>() {{
        put("SETTLEMENT_DETAIL", "会议结算单明细");
        put("VENUE_CONFIRMATION", "会场确认函、会场协议");
        put("CONSTRUCTION_DETAIL", "会议搭建明细");
        put("ACCOMMODATION_DETAIL", "住宿明细");
        put("CATERING_DETAIL", "餐饮明细");
        put("LOCAL_TRANSPORT_DETAIL", "小交通明细");
        put("INTERCITY_TRANSPORT_DETAIL", "大交通明细");
        put("MATERIAL_DETAIL", "物料明细");
        put("DESIGN_DRAFT_DETAIL", "设计稿明细");
        put("OTHER", "其他");
    }};

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

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

    public byte[] buildZip(Long tenantId, Long meetingId) {
        Map<String, Object> meeting = findMeeting(tenantId, meetingId);
        String meetingTopic = stringValue(meeting.get("topic"));
        String exportedAt = DATE_TIME_FORMATTER.format(LocalDateTime.now());
        String rootFolder = "会议资料包/" + sanitizeSegment((meetingTopic.isEmpty() ? "会议" : meetingTopic) + "-" + meetingId) + "/";
        List<Map<String, Object>> materialRows = jdbcTemplate.queryForList(
                "SELECT module_code, content_json FROM meeting_material WHERE tenant_id=? AND meeting_id=? AND is_deleted=0 ORDER BY id ASC",
                tenantId,
                meetingId
        );
        Map<String, String> materialJsonByCode = new LinkedHashMap<String, String>();
        for (Map<String, Object> row : materialRows) {
            String moduleCode = stringValue(row.get("module_code"));
            if (!moduleCode.isEmpty()) {
                materialJsonByCode.put(moduleCode, stringValue(row.get("content_json")));
            }
        }

        List<Map<String, Object>> manifestModules = new ArrayList<Map<String, Object>>();
        List<Map<String, Object>> manifestFiles = new ArrayList<Map<String, Object>>();
        List<String> failureLines = new ArrayList<String>();
        Set<String> usedEntries = new LinkedHashSet<String>();
        int attemptedAttachmentCount = 0;
        int successAttachmentCount = 0;
        int failedAttachmentCount = 0;

        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
             ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) {
            for (ModuleSpec moduleSpec : MODULE_SPECS) {
                String contentJson = materialJsonByCode.get(moduleSpec.moduleCode);
                if (contentJson == null || contentJson.trim().isEmpty()) {
                    continue;
                }
                Map<String, Object> moduleManifest = new LinkedHashMap<String, Object>();
                moduleManifest.put("moduleCode", moduleSpec.moduleCode);
                moduleManifest.put("moduleTitle", moduleSpec.folderName.substring(3));

                String moduleFolder = rootFolder + moduleSpec.folderName + "/";
                String jsonEntryPath = uniqueEntryPath(moduleFolder, moduleSpec.jsonFileName, usedEntries);
                writeTextEntry(zipOutputStream, jsonEntryPath, prettyJson(contentJson));
                manifestFiles.add(buildManifestFile(moduleSpec.moduleCode, moduleSpec.folderName.substring(3), "JSON", moduleSpec.jsonFileName, null, jsonEntryPath, "SUCCESS", null));

                Map<String, Object> parsedJson = parseJsonObject(contentJson, moduleSpec.moduleCode, failureLines);
                List<ExportAttachment> attachments = extractAttachments(moduleSpec.moduleCode, parsedJson);
                moduleManifest.put("jsonEntryPath", jsonEntryPath);
                moduleManifest.put("attachmentCount", attachments.size());

                for (ExportAttachment attachment : attachments) {
                    attemptedAttachmentCount++;
                    String displayName = resolveDisplayName(attachment.fileName, attachment.objectKey, attachment.fallbackName);
                    String outputFileName = withOptionalPrefix(attachment.fileNamePrefix, displayName);
                    String folderPath = moduleFolder + sanitizeSegment(attachment.subFolder) + "/";
                    String zipEntryPath = uniqueEntryPath(folderPath, outputFileName, usedEntries);
                    try {
                        byte[] fileBytes = ossService.getObjectBytes(attachment.objectKey);
                        writeBinaryEntry(zipOutputStream, zipEntryPath, fileBytes);
                        manifestFiles.add(buildManifestFile(moduleSpec.moduleCode, moduleSpec.folderName.substring(3), attachment.subFolder, outputFileName, attachment.objectKey, zipEntryPath, "SUCCESS", null));
                        successAttachmentCount++;
                    } catch (Exception ex) {
                        String errorMessage = ex.getMessage() == null ? "下载失败" : ex.getMessage();
                        failureLines.add(moduleSpec.folderName.substring(3) + "/" + attachment.subFolder + "/" + outputFileName + "：" + errorMessage);
                        manifestFiles.add(buildManifestFile(moduleSpec.moduleCode, moduleSpec.folderName.substring(3), attachment.subFolder, outputFileName, attachment.objectKey, zipEntryPath, "FAILED", errorMessage));
                        failedAttachmentCount++;
                    }
                }
                moduleManifest.put("successAttachmentCount", successAttachmentCount);
                moduleManifest.put("failedAttachmentCount", failedAttachmentCount);
                manifestModules.add(moduleManifest);
            }

            if (attemptedAttachmentCount > 0 && successAttachmentCount == 0) {
                throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "会议资料附件全部导出失败");
            }

            writeTextEntry(zipOutputStream, rootFolder + "00_导出说明.txt", buildReadme(meetingId, meetingTopic, exportedAt, attemptedAttachmentCount, successAttachmentCount, failedAttachmentCount, failureLines));

            Map<String, Object> manifest = new LinkedHashMap<String, Object>();
            manifest.put("meetingId", meetingId);
            manifest.put("meetingTopic", meetingTopic);
            manifest.put("exportedAt", exportedAt);
            manifest.put("attemptedAttachmentCount", attemptedAttachmentCount);
            manifest.put("successAttachmentCount", successAttachmentCount);
            manifest.put("failedAttachmentCount", failedAttachmentCount);
            manifest.put("modules", manifestModules);
            manifest.put("files", manifestFiles);
            writeTextEntry(zipOutputStream, rootFolder + "00_manifest.json", objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(manifest));

            zipOutputStream.finish();
            return outputStream.toByteArray();
        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "会议资料打包失败");
        }
    }

    private Map<String, Object> findMeeting(Long tenantId, Long meetingId) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT id, topic FROM meeting WHERE tenant_id=? AND id=? AND is_deleted=0 LIMIT 1",
                tenantId,
                meetingId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "会议不存在");
        }
        return rows.get(0);
    }

    private Map<String, Object> parseJsonObject(String contentJson, String moduleCode, List<String> failureLines) {
        try {
            return objectMapper.readValue(contentJson, new TypeReference<Map<String, Object>>() {});
        } catch (Exception ex) {
            failureLines.add(moduleCode + " 模块数据解析失败：" + ex.getMessage());
            return new LinkedHashMap<String, Object>();
        }
    }

    private List<ExportAttachment> extractAttachments(String moduleCode, Map<String, Object> content) {
        if ("WRITE_OFF_DOCS".equalsIgnoreCase(moduleCode)) {
            return extractWriteOffDocs(content);
        }
        if ("EXPERT_LIST".equalsIgnoreCase(moduleCode)) {
            return extractExpertList(content);
        }
        if ("MEETING_INVOICE".equalsIgnoreCase(moduleCode)) {
            return extractMeetingInvoices(content);
        }
        return Collections.emptyList();
    }

    private List<ExportAttachment> extractWriteOffDocs(Map<String, Object> content) {
        List<ExportAttachment> attachments = new ArrayList<ExportAttachment>();
        List<Map<String, Object>> agendas = listOfMap(content.get("agenda"));
        for (int i = 0; i < agendas.size(); i++) {
            Map<String, Object> agenda = agendas.get(i);
            attachments.add(new ExportAttachment(
                    "会议议程",
                    stringValue(agenda.get("name")),
                    stringValue(agenda.get("ossKey")),
                    "会议议程" + (i + 1),
                    null
            ));
        }

        Map<String, Object> signInSheet = mapValue(content.get("signInSheet"));
        if (!signInSheet.isEmpty()) {
            attachments.add(new ExportAttachment(
                    "签到表",
                    stringValue(signInSheet.get("name")),
                    stringValue(signInSheet.get("ossKey")),
                    "签到表",
                    null
            ));
        }

        Map<String, Object> themePhoto = mapValue(content.get("themePhoto"));
        if (!stringValue(themePhoto.get("ossKey")).isEmpty()) {
            attachments.add(new ExportAttachment(
                    "主题照片",
                    stringValue(firstNonEmpty(themePhoto.get("name"), themePhoto.get("fileName"))),
                    stringValue(themePhoto.get("ossKey")),
                    "主题照片",
                    null
            ));
        }

        List<Map<String, Object>> invitations = listOfMap(content.get("invitation"));
        for (int i = 0; i < invitations.size(); i++) {
            Map<String, Object> invitation = invitations.get(i);
            attachments.add(new ExportAttachment(
                    "邀请函",
                    stringValue(invitation.get("name")),
                    stringValue(invitation.get("ossKey")),
                    "邀请函" + (i + 1),
                    null
            ));
        }

        Map<String, Object> profileFile = mapValue(content.get("profileFile"));
        if (!profileFile.isEmpty()) {
            attachments.add(new ExportAttachment(
                    "专家简介串场稿",
                    stringValue(profileFile.get("name")),
                    stringValue(profileFile.get("ossKey")),
                    "专家简介串场稿",
                    null
            ));
        }
        return filterValidAttachments(attachments);
    }

    private List<ExportAttachment> extractExpertList(Map<String, Object> content) {
        List<ExportAttachment> attachments = new ArrayList<ExportAttachment>();
        Map<String, Object> onsitePhoto = mapValue(content.get("onsitePhoto"));
        List<Map<String, Object>> photos = listOfMap(onsitePhoto.get("photos"));
        for (int i = 0; i < photos.size(); i++) {
            Map<String, Object> photo = photos.get(i);
            attachments.add(new ExportAttachment(
                    "现场照片/" + resolveExpertFolder(photo, i + 1),
                    stringValue(photo.get("name")),
                    stringValue(photo.get("ossKey")),
                    "现场照片" + (i + 1),
                    null
            ));
        }

        Map<String, Object> laborProtocol = mapValue(content.get("laborProtocol"));
        List<Map<String, Object>> details = listOfMap(laborProtocol.get("details"));
        for (int i = 0; i < details.size(); i++) {
            Map<String, Object> detail = details.get(i);
            String expertFolder = resolveExpertFolder(detail, i + 1);
            Map<String, Object> protocolFile = mapValue(detail.get("protocolFile"));
            if (!protocolFile.isEmpty()) {
                attachments.add(new ExportAttachment(
                        "劳务协议/" + expertFolder,
                        stringValue(firstNonEmpty(protocolFile.get("fileName"), protocolFile.get("name"))),
                        stringValue(protocolFile.get("ossKey")),
                        "劳务协议" + (i + 1),
                        null
                ));
            }
            Map<String, Object> invoiceFile = mapValue(detail.get("invoiceFile"));
            if (!invoiceFile.isEmpty()) {
                attachments.add(new ExportAttachment(
                        "劳务协议/" + expertFolder,
                        stringValue(firstNonEmpty(invoiceFile.get("fileName"), invoiceFile.get("name"))),
                        stringValue(invoiceFile.get("ossKey")),
                        "劳务发票" + (i + 1),
                        null
                ));
            }
        }

        Map<String, Object> invoiceDetail = mapValue(content.get("invoiceDetail"));
        List<Map<String, Object>> invoices = listOfMap(invoiceDetail.get("invoices"));
        for (int i = 0; i < invoices.size(); i++) {
            Map<String, Object> invoice = invoices.get(i);
            String expertFolder = resolveExpertFolder(invoice, i + 1);
            String prefix = invoicePrefix(invoice);
            List<Map<String, Object>> files = listOfMap(invoice.get("files"));
            if (files.isEmpty()) {
                Map<String, Object> singleFile = mapValue(invoice.get("file"));
                if (!singleFile.isEmpty()) {
                    files = Collections.singletonList(singleFile);
                }
            }
            for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
                Map<String, Object> file = files.get(fileIndex);
                attachments.add(new ExportAttachment(
                        "专家发票/" + expertFolder,
                        stringValue(firstNonEmpty(file.get("fileName"), file.get("name"))),
                        stringValue(file.get("ossKey")),
                        "专家发票" + (i + 1) + "-" + (fileIndex + 1),
                        prefix
                ));
            }
        }
        return filterValidAttachments(attachments);
    }

    private List<ExportAttachment> extractMeetingInvoices(Map<String, Object> content) {
        List<ExportAttachment> attachments = new ArrayList<ExportAttachment>();
        List<Map<String, Object>> sections = listOfMap(content.get("sections"));
        for (int i = 0; i < sections.size(); i++) {
            Map<String, Object> section = sections.get(i);
            String sectionCode = stringValue(section.get("sectionCode"));
            String sectionTitle = stringValue(firstNonEmpty(section.get("sectionTitle"), INVOICE_SECTION_TITLES.get(sectionCode)));
            if (sectionTitle.isEmpty()) {
                sectionTitle = "发票分组" + (i + 1);
            }
            List<Map<String, Object>> files = listOfMap(section.get("files"));
            for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) {
                Map<String, Object> file = files.get(fileIndex);
                attachments.add(new ExportAttachment(
                        sectionTitle,
                        stringValue(file.get("fileName")),
                        stringValue(file.get("ossKey")),
                        sectionTitle + (fileIndex + 1),
                        stringValue(file.get("label"))
                ));
            }
        }
        return filterValidAttachments(attachments);
    }

    private List<ExportAttachment> filterValidAttachments(List<ExportAttachment> attachments) {
        List<ExportAttachment> valid = new ArrayList<ExportAttachment>();
        for (ExportAttachment attachment : attachments) {
            if (attachment == null) {
                continue;
            }
            String subFolder = sanitizeNestedFolder(attachment.subFolder);
            valid.add(new ExportAttachment(subFolder, attachment.fileName, attachment.objectKey, attachment.fallbackName, attachment.fileNamePrefix));
        }
        return valid;
    }

    private String resolveExpertFolder(Map<String, Object> row, int fallbackIndex) {
        String expertName = stringValue(firstNonEmpty(row.get("expertName"), row.get("name")));
        if (!expertName.isEmpty()) {
            return sanitizeSegment(expertName);
        }
        Long expertId = longValue(row.get("expertId"));
        if (expertId != null && expertId > 0L) {
            return "专家-" + expertId;
        }
        return "专家-" + fallbackIndex;
    }

    private String invoicePrefix(Map<String, Object> invoice) {
        List<String> parts = new ArrayList<String>();
        String expenseType = stringValue(invoice.get("expenseType"));
        if (!expenseType.isEmpty()) {
            parts.add(expenseType);
        }
        String invoiceNo = stringValue(invoice.get("invoiceNo"));
        if (!invoiceNo.isEmpty()) {
            parts.add(invoiceNo);
        }
        return parts.isEmpty() ? null : String.join("-", parts);
    }

    private Map<String, Object> buildManifestFile(String moduleCode,
                                                  String moduleTitle,
                                                  String subFolder,
                                                  String fileName,
                                                  String objectKey,
                                                  String zipEntryPath,
                                                  String status,
                                                  String errorMessage) {
        Map<String, Object> file = new LinkedHashMap<String, Object>();
        file.put("moduleCode", moduleCode);
        file.put("moduleTitle", moduleTitle);
        file.put("subFolder", subFolder);
        file.put("displayName", fileName);
        file.put("objectKey", objectKey);
        file.put("zipEntryPath", zipEntryPath);
        file.put("status", status);
        file.put("errorMessage", errorMessage);
        return file;
    }

    private void writeTextEntry(ZipOutputStream zipOutputStream, String entryPath, String content) throws IOException {
        writeBinaryEntry(zipOutputStream, entryPath, content == null ? new byte[0] : content.getBytes(StandardCharsets.UTF_8));
    }

    private void writeBinaryEntry(ZipOutputStream zipOutputStream, String entryPath, byte[] bytes) throws IOException {
        ZipEntry entry = new ZipEntry(entryPath);
        zipOutputStream.putNextEntry(entry);
        zipOutputStream.write(bytes == null ? new byte[0] : bytes);
        zipOutputStream.closeEntry();
    }

    private String buildReadme(Long meetingId,
                               String meetingTopic,
                               String exportedAt,
                               int attemptedAttachmentCount,
                               int successAttachmentCount,
                               int failedAttachmentCount,
                               List<String> failureLines) {
        StringBuilder builder = new StringBuilder();
        builder.append("会议资料包导出说明").append("\r\n");
        builder.append("会议ID：").append(meetingId).append("\r\n");
        builder.append("会议主题：").append(meetingTopic == null || meetingTopic.trim().isEmpty() ? "-" : meetingTopic.trim()).append("\r\n");
        builder.append("导出时间：").append(exportedAt).append("\r\n");
        builder.append("附件总数：").append(attemptedAttachmentCount).append("\r\n");
        builder.append("成功数量：").append(successAttachmentCount).append("\r\n");
        builder.append("失败数量：").append(failedAttachmentCount).append("\r\n");
        builder.append("\r\n");
        builder.append("失败明细：").append("\r\n");
        if (failureLines.isEmpty()) {
            builder.append("无").append("\r\n");
        } else {
            for (String failureLine : failureLines) {
                builder.append("- ").append(failureLine).append("\r\n");
            }
        }
        return builder.toString();
    }

    private String prettyJson(String contentJson) {
        try {
            Object parsed = objectMapper.readValue(contentJson, Object.class);
            return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parsed);
        } catch (Exception ex) {
            return contentJson == null ? "" : contentJson;
        }
    }

    private String resolveDisplayName(String fileName, String objectKey, String fallbackName) {
        String displayName = sanitizeFileName(fileName);
        if (!displayName.isEmpty()) {
            return appendExtensionIfMissing(displayName, objectKey);
        }
        String keyName = sanitizeFileName(fileNameFromObjectKey(objectKey));
        if (!keyName.isEmpty()) {
            return keyName;
        }
        return appendExtensionIfMissing(sanitizeFileName(fallbackName), objectKey);
    }

    private String appendExtensionIfMissing(String fileName, String objectKey) {
        String safeFileName = sanitizeFileName(fileName);
        if (safeFileName.isEmpty() || hasFileExtension(safeFileName)) {
            return safeFileName;
        }
        String objectFileName = sanitizeFileName(fileNameFromObjectKey(objectKey));
        String extension = fileExtension(objectFileName);
        if (extension.isEmpty()) {
            return safeFileName;
        }
        return safeFileName + extension;
    }

    private String uniqueEntryPath(String directory, String fileName, Set<String> usedEntries) {
        String safeDirectory = directory;
        String safeFileName = sanitizeFileName(fileName);
        if (safeFileName.isEmpty()) {
            safeFileName = "附件";
        }
        String extension = "";
        String baseName = safeFileName;
        int dotIndex = safeFileName.lastIndexOf('.');
        if (dotIndex > 0 && dotIndex < safeFileName.length() - 1) {
            extension = safeFileName.substring(dotIndex);
            baseName = safeFileName.substring(0, dotIndex);
        }
        String candidate = safeDirectory + safeFileName;
        int suffix = 2;
        while (!usedEntries.add(candidate)) {
            candidate = safeDirectory + baseName + "(" + suffix + ")" + extension;
            suffix++;
        }
        return candidate;
    }

    private String sanitizeNestedFolder(String rawFolder) {
        String text = rawFolder == null ? "" : rawFolder.trim();
        if (text.isEmpty()) {
            return "未分类";
        }
        String[] parts = text.split("/");
        List<String> safeParts = new ArrayList<String>();
        for (String part : parts) {
            String safe = sanitizeSegment(part);
            if (!safe.isEmpty()) {
                safeParts.add(safe);
            }
        }
        return safeParts.isEmpty() ? "未分类" : String.join("/", safeParts);
    }

    private String sanitizeSegment(String value) {
        String text = value == null ? "" : value.trim();
        if (text.isEmpty()) {
            return "";
        }
        String safe = ILLEGAL_FILE_CHARS.matcher(text).replaceAll("_");
        safe = safe.replaceAll("\\s+", " ").trim();
        return safe.isEmpty() ? "" : safe;
    }

    private String sanitizeFileName(String value) {
        String safe = sanitizeSegment(value);
        if (safe.isEmpty()) {
            return "";
        }
        if (".".equals(safe) || "..".equals(safe)) {
            return "附件";
        }
        return safe;
    }

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

    private boolean hasFileExtension(String fileName) {
        return !fileExtension(fileName).isEmpty();
    }

    private String fileExtension(String fileName) {
        String safeFileName = sanitizeFileName(fileName);
        if (safeFileName.isEmpty()) {
            return "";
        }
        int dotIndex = safeFileName.lastIndexOf('.');
        if (dotIndex <= 0 || dotIndex >= safeFileName.length() - 1) {
            return "";
        }
        return safeFileName.substring(dotIndex);
    }

    private String withOptionalPrefix(String prefix, String fileName) {
        String safeFileName = sanitizeFileName(fileName);
        String safePrefix = sanitizeSegment(prefix);
        if (safePrefix.isEmpty()) {
            return safeFileName;
        }
        if (safeFileName.isEmpty()) {
            return safePrefix;
        }
        return safePrefix + "-" + safeFileName;
    }

    private Object firstNonEmpty(Object first, Object second) {
        String firstText = stringValue(first);
        if (!firstText.isEmpty()) {
            return first;
        }
        return second;
    }

    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;
    }

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

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

    private Long longValue(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 class ModuleSpec {
        private final String moduleCode;
        private final String folderName;
        private final String jsonFileName;

        private ModuleSpec(String moduleCode, String folderName, String jsonFileName) {
            this.moduleCode = moduleCode;
            this.folderName = folderName;
            this.jsonFileName = jsonFileName;
        }
    }

    private static class ExportAttachment {
        private final String subFolder;
        private final String fileName;
        private final String objectKey;
        private final String fallbackName;
        private final String fileNamePrefix;

        private ExportAttachment(String subFolder, String fileName, String objectKey, String fallbackName, String fileNamePrefix) {
            this.subFolder = subFolder;
            this.fileName = fileName;
            this.objectKey = objectKey;
            this.fallbackName = fallbackName;
            this.fileNamePrefix = fileNamePrefix;
        }
    }
}
