package com.writeoff.module.export.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.writeoff.common.api.PageResult;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.writeoff.module.file.service.OssService;
import com.writeoff.module.export.dto.CreateExportTaskRequest;
import com.writeoff.module.export.model.ExportTaskInfo;
import com.writeoff.module.meeting.service.MeetingMaterialExportService;
import com.writeoff.module.meeting.service.MeetingSummaryExportService;
import com.writeoff.module.scheduler.service.AsyncJobService;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
import java.util.stream.Collectors;
import java.nio.charset.StandardCharsets;

@Service
public class ExportTaskService {
    private final JdbcTemplate jdbcTemplate;
    private final AsyncJobService asyncJobService;
    private final OssService ossService;
    private final MeetingMaterialExportService meetingMaterialExportService;
    private final MeetingSummaryExportService meetingSummaryExportService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    private static final RowMapper<ExportTaskInfo> ROW_MAPPER = (rs, n) -> new ExportTaskInfo(
            rs.getLong("id"),
            rs.getString("task_code"),
            rs.getString("biz_type"),
            rs.getString("biz_id"),
            rs.getString("file_name"),
            rs.getString("file_oss_key"),
            rs.getString("status"),
            rs.getInt("retry_count"),
            rs.getInt("download_count"),
            rs.getString("token_expire_at"),
            rs.getString("error_message"),
            rs.getString("created_at"),
            rs.getString("finished_at")
    );

    public ExportTaskService(JdbcTemplate jdbcTemplate,
                             AsyncJobService asyncJobService,
                             OssService ossService,
                             MeetingMaterialExportService meetingMaterialExportService,
                             MeetingSummaryExportService meetingSummaryExportService) {
        this.jdbcTemplate = jdbcTemplate;
        this.asyncJobService = asyncJobService;
        this.ossService = ossService;
        this.meetingMaterialExportService = meetingMaterialExportService;
        this.meetingSummaryExportService = meetingSummaryExportService;
    }

    public PageResult<ExportTaskInfo> list() {
        List<ExportTaskInfo> list = jdbcTemplate.query(
                "SELECT id, task_code, biz_type, biz_id, file_name, file_oss_key, status, retry_count, IFNULL(download_count,0) AS download_count, " +
                        "DATE_FORMAT(download_token_expire_at, '%Y-%m-%d %H:%i:%s') AS token_expire_at, error_message, " +
                        "DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS created_at, DATE_FORMAT(finished_at, '%Y-%m-%d %H:%i:%s') AS finished_at " +
                        "FROM export_task WHERE tenant_id=? AND is_deleted=0 ORDER BY id DESC LIMIT 300",
                ROW_MAPPER,
                tenantId()
        );
        return new PageResult<ExportTaskInfo>(list, list.size(), 1, 300);
    }

    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> create(CreateExportTaskRequest request) {
        String fileName = request.getFileName() == null || request.getFileName().trim().isEmpty()
                ? (request.getTaskCode() + "-" + System.currentTimeMillis() + ".csv")
                : request.getFileName().trim();
        jdbcTemplate.update(
                "INSERT INTO export_task (tenant_id, task_code, biz_type, biz_id, filters_json, file_name, status, retry_count, max_retry, idempotency_key, requested_by, created_by, updated_by) " +
                        "VALUES (?, ?, ?, ?, ?, ?, 'PENDING', 0, 3, ?, ?, ?, ?)",
                tenantId(),
                request.getTaskCode(),
                request.getBizType(),
                request.getBizId(),
                request.getFiltersJson(),
                fileName,
                request.getIdempotencyKey(),
                safeUserId(),
                safeUserId(),
                safeUserId()
        );
        Long taskId = jdbcTemplate.queryForObject("SELECT IFNULL(MAX(id),0) FROM export_task WHERE tenant_id=?", Long.class, tenantId());
        Long id = taskId == null ? 0L : taskId;
        Map<String, Object> payload = new LinkedHashMap<String, Object>();
        payload.put("taskId", id);
        try {
            asyncJobService.enqueue("EXPORT_TASK", objectMapper.writeValueAsString(payload), "export-task-" + id);
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "导出任务入队失败");
        }
        Map<String, Object> result = new LinkedHashMap<String, Object>();
        result.put("taskId", id);
        result.put("status", "PENDING");
        return result;
    }

    public void processTask(String payload) {
        Long taskId = parseTaskId(payload);
        Map<String, Object> task = findTaskDetail(taskId);
        String status = String.valueOf(task.get("status"));
        if ("SUCCESS".equalsIgnoreCase(status)) {
            return;
        }
        int retryCount = ((Number) task.get("retry_count")).intValue();
        int maxRetry = ((Number) task.get("max_retry")).intValue();
        Long taskTenantId = toLong(task.get("tenant_id"));
        Long requestedBy = toLong(task.get("requested_by"));
        try {
            ExportContent exportContent = buildExportContent(taskTenantId, task);
            String fileOssKey = "exports/" + (taskTenantId == null ? tenantId() : taskTenantId) + "/" + taskId + "-" + System.currentTimeMillis() + exportContent.extension;
            ossService.putObject(fileOssKey, exportContent.bytes, exportContent.contentType);
            String token = UUID.randomUUID().toString().replace("-", "");
            jdbcTemplate.update(
                    "UPDATE export_task SET status='SUCCESS', file_oss_key=?, download_token=?, download_token_expire_at=DATE_ADD(NOW(), INTERVAL 7 DAY), error_message=NULL, finished_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP, updated_by=? WHERE tenant_id=? AND id=?",
                    fileOssKey,
                    token,
                    requestedBy == null ? 0L : requestedBy,
                    taskTenantId,
                    taskId
            );
        } catch (Exception ex) {
            int nextRetry = retryCount + 1;
            String nextStatus = nextRetry >= maxRetry ? "FAILED" : "PENDING";
            jdbcTemplate.update(
                    "UPDATE export_task SET status=?, retry_count=?, error_message=?, updated_at=CURRENT_TIMESTAMP, updated_by=? WHERE tenant_id=? AND id=?",
                    nextStatus,
                    nextRetry,
                    truncateErrorMessage(ex),
                    requestedBy == null ? 0L : requestedBy,
                    taskTenantId,
                    taskId
            );
            throw new IllegalStateException("导出任务处理失败: " + truncateErrorMessage(ex), ex);
        }
    }

    private ExportContent buildExportContent(Long taskTenantId, Map<String, Object> task) {
        String taskCode = String.valueOf(task.get("task_code"));
        String fileName = task.get("file_name") == null ? "" : String.valueOf(task.get("file_name"));
        if ("MEETING_MATERIAL_EXPORT".equalsIgnoreCase(taskCode)) {
            Map<String, Object> filters = parseJsonMap(task.get("filters_json"));
            Long meetingId = toLong(filters.get("meetingId"));
            if (meetingId == null || meetingId <= 0L) {
                meetingId = toLong(task.get("biz_id"));
            }
            if (meetingId == null || meetingId <= 0L) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "会议资料导出缺少会议ID");
            }
            byte[] zipBytes = meetingMaterialExportService.buildZip(taskTenantId, meetingId);
            return new ExportContent(zipBytes, "application/zip", resolveExtension(fileName, ".zip"));
        }
        if ("MEETING_SUMMARY_GENERATE".equalsIgnoreCase(taskCode)) {
            Map<String, Object> filters = parseJsonMap(task.get("filters_json"));
            Long meetingId = toLong(filters.get("meetingId"));
            if (meetingId == null || meetingId <= 0L) {
                meetingId = toLong(task.get("biz_id"));
            }
            if (meetingId == null || meetingId <= 0L) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "会议总结导出缺少会议ID");
            }
            byte[] docxBytes = meetingSummaryExportService.buildDocx(taskTenantId, meetingId);
            return new ExportContent(
                    docxBytes,
                    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                    resolveExtension(fileName, ".docx")
            );
        }
        String csvContent = buildCsvContent(taskTenantId, task);
        return new ExportContent(csvContent.getBytes(StandardCharsets.UTF_8), "text/csv;charset=utf-8", resolveExtension(fileName, ".csv"));
    }

    public Map<String, Object> refreshDownloadToken(Long taskId) {
        Map<String, Object> task = findTaskWithOwner(taskId);
        assertOwner(task);
        String status = String.valueOf(task.get("status"));
        if (!"SUCCESS".equalsIgnoreCase(status)) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "导出任务未完成，不能刷新下载令牌");
        }
        String token = UUID.randomUUID().toString().replace("-", "");
        jdbcTemplate.update(
                "UPDATE export_task SET download_token=?, download_token_expire_at=DATE_ADD(NOW(), INTERVAL 7 DAY), updated_at=CURRENT_TIMESTAMP, updated_by=? WHERE tenant_id=? AND id=?",
                token,
                safeUserId(),
                tenantId(),
                taskId
        );
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("taskId", taskId);
        data.put("downloadToken", token);
        data.put("expireAt", jdbcTemplate.queryForObject("SELECT DATE_FORMAT(download_token_expire_at, '%Y-%m-%d %H:%i:%s') FROM export_task WHERE tenant_id=? AND id=?", String.class, tenantId(), taskId));
        return data;
    }

    public Map<String, Object> download(Long taskId, String token) {
        if (token == null || token.trim().isEmpty()) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "下载令牌不能为空");
        }
        Map<String, Object> task = findTaskWithOwner(taskId);
        assertOwner(task);
        String status = String.valueOf(task.get("status"));
        if (!"SUCCESS".equalsIgnoreCase(status)) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "导出任务未完成");
        }
        String dbToken = task.get("download_token") == null ? "" : String.valueOf(task.get("download_token"));
        if (!dbToken.equals(token)) {
            throw new BusinessException(ErrorCodes.NO_DATA_PERMISSION, "下载令牌无效");
        }
        Integer valid = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM export_task WHERE tenant_id=? AND id=? AND download_token_expire_at IS NOT NULL AND download_token_expire_at>=NOW()",
                Integer.class,
                tenantId(),
                taskId
        );
        if (valid == null || valid == 0) {
            throw new BusinessException(ErrorCodes.NO_DATA_PERMISSION, "下载令牌已过期");
        }
        jdbcTemplate.update(
                "UPDATE export_task SET download_count=IFNULL(download_count,0)+1, updated_at=CURRENT_TIMESTAMP, updated_by=? WHERE tenant_id=? AND id=?",
                safeUserId(),
                tenantId(),
                taskId
        );
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("taskId", taskId);
        data.put("fileName", task.get("file_name"));
        data.put("fileOssKey", task.get("file_oss_key"));
        data.put("signedUrl", ossService.generateDownloadUrl(String.valueOf(task.get("file_oss_key")), String.valueOf(task.get("file_name"))));
        data.put("expireAt", jdbcTemplate.queryForObject("SELECT DATE_FORMAT(download_token_expire_at, '%Y-%m-%d %H:%i:%s') FROM export_task WHERE tenant_id=? AND id=?", String.class, tenantId(), taskId));
        data.put("downloadCount", jdbcTemplate.queryForObject("SELECT IFNULL(download_count,0) FROM export_task WHERE tenant_id=? AND id=?", Integer.class, tenantId(), taskId));
        return data;
    }

    private Long parseTaskId(String payload) {
        try {
            Map<String, Object> map = objectMapper.readValue(payload, new TypeReference<Map<String, Object>>() {});
            Object val = map.get("taskId");
            if (val == null) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "导出任务ID缺失");
            }
            return Long.valueOf(String.valueOf(val));
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "导出任务参数非法");
        }
    }

    private Map<String, Object> findTask(Long taskId) {
        List<Map<String, Object>> list = jdbcTemplate.queryForList(
                "SELECT id, status, retry_count, max_retry FROM export_task WHERE tenant_id=? AND id=? AND is_deleted=0",
                tenantId(),
                taskId
        );
        if (list.isEmpty()) {
            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "导出任务不存在");
        }
        return list.get(0);
    }

    private Map<String, Object> findTaskDetail(Long taskId) {
        List<Map<String, Object>> list = jdbcTemplate.queryForList(
                "SELECT id, tenant_id, status, retry_count, max_retry, requested_by, task_code, biz_type, biz_id, filters_json, file_name " +
                        "FROM export_task WHERE tenant_id=? AND id=? AND is_deleted=0",
                tenantId(),
                taskId
        );
        if (list.isEmpty()) {
            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "导出任务不存在");
        }
        return list.get(0);
    }

    private Map<String, Object> findTaskWithOwner(Long taskId) {
        List<Map<String, Object>> list = jdbcTemplate.queryForList(
                "SELECT id, status, retry_count, max_retry, requested_by, file_name, file_oss_key, download_token FROM export_task WHERE tenant_id=? AND id=? AND is_deleted=0",
                tenantId(),
                taskId
        );
        if (list.isEmpty()) {
            throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "导出任务不存在");
        }
        return list.get(0);
    }

    private void assertOwner(Map<String, Object> task) {
        Long requestedBy = task.get("requested_by") == null ? 0L : ((Number) task.get("requested_by")).longValue();
        if (!requestedBy.equals(safeUserId())) {
            throw new BusinessException(ErrorCodes.NO_DATA_PERMISSION, "仅任务创建人可下载导出结果");
        }
    }

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

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

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

    private String buildCsvContent(Long taskTenantId, Map<String, Object> task) {
        String taskCode = String.valueOf(task.get("task_code"));
        Map<String, Object> filters = parseJsonMap(task.get("filters_json"));
        if ("MEETING_EXPORT".equalsIgnoreCase(taskCode)) {
            return buildMeetingCsv(taskTenantId, filters);
        }
        if ("USER_EXPORT".equalsIgnoreCase(taskCode)) {
            return buildUserCsv(taskTenantId, filters);
        }
        if ("PROJECT_EXPORT".equalsIgnoreCase(taskCode)) {
            return buildProjectCsv(taskTenantId, filters);
        }
        if ("NOTIFICATION_TASK_EXPORT".equalsIgnoreCase(taskCode)) {
            return buildNotificationTaskCsv(taskTenantId, filters);
        }
        throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "不支持的导出任务类型: " + taskCode);
    }

    private Map<String, Object> parseJsonMap(Object raw) {
        if (raw == null) {
            return new LinkedHashMap<String, Object>();
        }
        String text = String.valueOf(raw).trim();
        if (text.isEmpty()) {
            return new LinkedHashMap<String, Object>();
        }
        try {
            return objectMapper.readValue(text, new TypeReference<Map<String, Object>>() {});
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "导出筛选条件格式不正确");
        }
    }

    private String buildMeetingCsv(Long tenantId, Map<String, Object> filters) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT m.id, p.project_name, m.topic, m.meeting_status, m.audit_status, ");
        sql.append("DATE_FORMAT(m.start_time, '%Y-%m-%d %H:%i:%s') AS start_time, ");
        sql.append("DATE_FORMAT(m.end_time, '%Y-%m-%d %H:%i:%s') AS end_time, ");
        sql.append("m.budget_cent, m.is_deleted ");
        sql.append("FROM meeting m LEFT JOIN project p ON m.tenant_id=p.tenant_id AND m.project_id=p.id ");
        sql.append("WHERE m.tenant_id=? ");
        List<Object> args = new ArrayList<Object>();
        args.add(tenantId);
        appendDeletedFilter(sql, args, "m.is_deleted", filters);
        appendLikeFilter(sql, args, "p.project_name", filters.get("projectName"));
        appendLikeFilter(sql, args, "m.topic", filters.get("topic"));
        appendEqualsFilter(sql, args, "m.project_id", filters.get("projectId"));
        appendEqualsFilter(sql, args, "m.meeting_status", filters.get("meetingStatus"));
        appendEqualsFilter(sql, args, "m.audit_status", filters.get("auditStatus"));
        sql.append(" ORDER BY m.id DESC");
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), args.toArray());
        return toCsv(
                Arrays.asList("会议ID", "项目名称", "会议主题", "会议状态", "审核状态", "开始时间", "结束时间", "预算(元)", "是否已删除"),
                rows.stream().map(row -> Arrays.asList(
                        row.get("id"),
                        row.get("project_name"),
                        row.get("topic"),
                        row.get("meeting_status"),
                        row.get("audit_status"),
                        row.get("start_time"),
                        row.get("end_time"),
                        toYuan(row.get("budget_cent")),
                        toDeletedText(row.get("is_deleted"))
                )).collect(Collectors.<List<Object>>toList())
        );
    }

    private String buildUserCsv(Long tenantId, Map<String, Object> filters) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT u.id, u.user_name, u.phone, u.email, u.status, ");
        sql.append("DATE_FORMAT(u.valid_from, '%Y-%m-%d %H:%i:%s') AS valid_from, ");
        sql.append("DATE_FORMAT(u.valid_to, '%Y-%m-%d %H:%i:%s') AS valid_to, u.is_deleted, ");
        sql.append("COALESCE(GROUP_CONCAT(DISTINCT r.role_name ORDER BY r.id SEPARATOR ','), '') AS role_names ");
        sql.append("FROM sys_user u ");
        sql.append("LEFT JOIN user_role ur ON u.tenant_id=ur.tenant_id AND u.id=ur.user_id ");
        sql.append("LEFT JOIN role r ON ur.tenant_id=r.tenant_id AND ur.role_id=r.id AND r.is_deleted=0 ");
        sql.append("WHERE u.tenant_id=? ");
        List<Object> args = new ArrayList<Object>();
        args.add(tenantId);
        appendDeletedFilter(sql, args, "u.is_deleted", filters);
        sql.append(" GROUP BY u.id, u.user_name, u.phone, u.email, u.status, u.valid_from, u.valid_to, u.is_deleted ");
        sql.append("ORDER BY u.id DESC");
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), args.toArray());
        return toCsv(
                Arrays.asList("用户ID", "姓名", "手机号", "邮箱", "状态", "角色", "生效时间", "失效时间", "是否已删除"),
                rows.stream().map(row -> Arrays.asList(
                        row.get("id"),
                        row.get("user_name"),
                        row.get("phone"),
                        row.get("email"),
                        row.get("status"),
                        row.get("role_names"),
                        row.get("valid_from"),
                        row.get("valid_to"),
                        toDeletedText(row.get("is_deleted"))
                )).collect(Collectors.<List<Object>>toList())
        );
    }

    private String buildProjectCsv(Long tenantId, Map<String, Object> filters) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT id, project_name, host_enterprise_name, budget_cent, meeting_total, status, ");
        sql.append("DATE_FORMAT(start_date, '%Y-%m-%d') AS start_date, DATE_FORMAT(end_date, '%Y-%m-%d') AS end_date, is_deleted ");
        sql.append("FROM project WHERE tenant_id=? ");
        List<Object> args = new ArrayList<Object>();
        args.add(tenantId);
        appendDeletedFilter(sql, args, "is_deleted", filters);
        appendLikeFilter(sql, args, "project_name", filters.get("projectName"));
        appendEqualsFilter(sql, args, "parent_project_id", filters.get("parentProjectId"));
        sql.append(" ORDER BY id DESC");
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), args.toArray());
        return toCsv(
                Arrays.asList("项目ID", "项目名称", "主办单位", "预算(元)", "会议总期数", "状态", "开始日期", "结束日期", "是否已删除"),
                rows.stream().map(row -> Arrays.asList(
                        row.get("id"),
                        row.get("project_name"),
                        row.get("host_enterprise_name"),
                        toYuan(row.get("budget_cent")),
                        row.get("meeting_total"),
                        row.get("status"),
                        row.get("start_date"),
                        row.get("end_date"),
                        toDeletedText(row.get("is_deleted"))
                )).collect(Collectors.<List<Object>>toList())
        );
    }

    private String buildNotificationTaskCsv(Long tenantId, Map<String, Object> filters) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT id, event_code, channel, receiver_type, receiver_ref, status, retry_count, ");
        sql.append("provider_message_id, receipt_code, receipt_message, error_message, ");
        sql.append("DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS created_at ");
        sql.append("FROM notification_task WHERE tenant_id=? ");
        List<Object> args = new ArrayList<Object>();
        args.add(tenantId);
        appendDeletedFilter(sql, args, "is_deleted", filters);
        sql.append(" ORDER BY id DESC");
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), args.toArray());
        return toCsv(
                Arrays.asList("任务ID", "事件编码", "渠道", "接收人类型", "接收人", "状态", "重试次数", "供应商消息ID", "回执码", "回执信息", "错误信息", "创建时间"),
                rows.stream().map(row -> Arrays.asList(
                        row.get("id"),
                        row.get("event_code"),
                        row.get("channel"),
                        row.get("receiver_type"),
                        row.get("receiver_ref"),
                        row.get("status"),
                        row.get("retry_count"),
                        row.get("provider_message_id"),
                        row.get("receipt_code"),
                        row.get("receipt_message"),
                        row.get("error_message"),
                        row.get("created_at")
                )).collect(Collectors.<List<Object>>toList())
        );
    }

    private void appendDeletedFilter(StringBuilder sql, List<Object> args, String column, Map<String, Object> filters) {
        boolean includeDeleted = filters != null && Boolean.TRUE.equals(parseBoolean(filters.get("includeDeleted")));
        if (!includeDeleted) {
            sql.append(" AND ").append(column).append("=0");
        }
    }

    private Boolean parseBoolean(Object value) {
        if (value instanceof Boolean) {
            return (Boolean) value;
        }
        if (value == null) {
            return false;
        }
        return "true".equalsIgnoreCase(String.valueOf(value));
    }

    private void appendLikeFilter(StringBuilder sql, List<Object> args, String column, Object value) {
        String text = value == null ? "" : String.valueOf(value).trim();
        if (!text.isEmpty()) {
            sql.append(" AND ").append(column).append(" LIKE ?");
            args.add("%" + text + "%");
        }
    }

    private void appendEqualsFilter(StringBuilder sql, List<Object> args, String column, Object value) {
        if (value == null || String.valueOf(value).trim().isEmpty()) {
            return;
        }
        sql.append(" AND ").append(column).append("=?");
        args.add(value);
    }

    private String toCsv(List<String> headers, List<List<Object>> rows) {
        StringBuilder builder = new StringBuilder();
        builder.append('\uFEFF');
        builder.append(headers.stream().map(this::escapeCsv).collect(Collectors.joining(","))).append("\r\n");
        for (List<Object> row : rows) {
            builder.append(row.stream()
                    .map(val -> escapeCsv(val == null ? "" : String.valueOf(val)))
                    .collect(Collectors.joining(",")));
            builder.append("\r\n");
        }
        return builder.toString();
    }

    private String escapeCsv(String value) {
        String text = value == null ? "" : value;
        String normalized = text.replace("\"", "\"\"");
        if (normalized.contains(",") || normalized.contains("\"") || normalized.contains("\n") || normalized.contains("\r")) {
            return "\"" + normalized + "\"";
        }
        return normalized;
    }

    private String toDeletedText(Object flag) {
        return toLong(flag) != null && toLong(flag) == 1L ? "是" : "否";
    }

    private String toYuan(Object cent) {
        long value = toLong(cent) == null ? 0L : toLong(cent);
        return String.format(Locale.ROOT, "%.2f", value / 100.0d);
    }

    private String resolveExtension(String fileName, String defaultExtension) {
        String text = fileName == null ? "" : fileName.trim();
        int idx = text.lastIndexOf('.');
        if (idx > 0 && idx < text.length() - 1) {
            return text.substring(idx);
        }
        return defaultExtension;
    }

    private String truncateErrorMessage(Exception ex) {
        String message = ex == null ? "" : String.valueOf(ex.getMessage());
        if (message == null || message.trim().isEmpty()) {
            message = ex == null ? "未知错误" : ex.getClass().getSimpleName();
        }
        String safe = message.trim();
        return safe.length() > 500 ? safe.substring(0, 500) : safe;
    }

    private static class ExportContent {
        private final byte[] bytes;
        private final String contentType;
        private final String extension;

        private ExportContent(byte[] bytes, String contentType, String extension) {
            this.bytes = bytes == null ? new byte[0] : bytes;
            this.contentType = contentType;
            this.extension = extension;
        }
    }
}
