package com.ruoyi.ccdi.project.service.impl;

import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.enums.TriggerType;
import com.ruoyi.ccdi.project.domain.dto.CcdiFileUploadQueryDTO;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
import com.ruoyi.ccdi.project.domain.excel.CcdiIdCardExcelRow;
import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
import com.ruoyi.lsfx.domain.request.GetFileUploadStatusRequest;
import com.ruoyi.lsfx.domain.request.DeleteFilesRequest;
import com.ruoyi.lsfx.domain.response.*;
import jakarta.annotation.Resource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.regex.Pattern;

/**
 * 文件上传服务实现
 *
 * @author ruoyi
 * @date 2026-03-05
 */
@Slf4j
@Service
public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {

    private static final int MAX_ERROR_MESSAGE_LENGTH = 2000;
    private static final Pattern ID_CARD_PATTERN =
        Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])([0-2]\\d|3[01])\\d{3}[0-9Xx]$");
    private static final Pattern UPLOAD_FILE_NAME_ID_CARD_PATTERN =
        Pattern.compile("(?<!\\d)\\d{17}[0-9Xx](?!\\d)");
    private static final Pattern FILE_NAME_SPACE_PATTERN = Pattern.compile("[\\s\\u3000]+");

    @Data
    private static class FetchBankStatementResult {
        private boolean success;
        private int totalCount;
        private int attemptedCount;
        private String errorMessage;
    }

    /**
     * 若依框架文件上传路径
     */
    @Value("${ruoyi.profile}")
    private String uploadPath;

    @Resource
    private CcdiFileUploadRecordMapper recordMapper;

    @Resource
    private CcdiProjectMapper projectMapper;

    @Resource
    @Qualifier("fileUploadExecutor")
    private Executor fileUploadExecutor;

    @Resource
    private LsfxAnalysisClient lsfxClient;

    @Resource
    private CcdiBankStatementMapper bankStatementMapper;

    @Resource
    private ICcdiBankTagService bankTagService;

    @Resource
    private ICcdiProjectService projectService;

    /**
     * 获取临时文件存储目录
     */
    private String getTempFileDir() {
        return uploadPath + File.separator + "temp";
    }

    @Override
    public List<String> parseIdCardFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new RuntimeException("身份证文件不能为空");
        }

        try {
            List<CcdiIdCardExcelRow> rows = EasyExcel.read(file.getInputStream())
                .head(CcdiIdCardExcelRow.class)
                .sheet(0)
                .headRowNumber(1)
                .doReadSync();

            LinkedHashSet<String> idCards = new LinkedHashSet<>();
            for (CcdiIdCardExcelRow row : rows) {
                if (row == null) {
                    continue;
                }
                String idCard = row.getIdCard();
                if (!StringUtils.hasText(idCard)) {
                    continue;
                }
                String normalized = idCard.trim();
                if (!ID_CARD_PATTERN.matcher(normalized).matches()) {
                    continue;
                }
                idCards.add(normalized);
            }

            if (idCards.isEmpty()) {
                throw new RuntimeException("首个sheet第一列未解析到有效身份证号");
            }
            return new ArrayList<>(idCards);
        } catch (IOException e) {
            throw new RuntimeException("读取身份证文件失败: " + e.getMessage(), e);
        }
    }

    @Transactional
    @Override
    public String submitPullBankInfo(Long projectId,
                                     List<String> idCards,
                                     String dataChannelCode,
                                     String startDate,
                                     String endDate,
                                     Long userId,
                                     String username) {
        if (projectId == null) {
            throw new IllegalArgumentException("项目ID不能为空");
        }
        if (idCards == null || idCards.isEmpty()) {
            throw new IllegalArgumentException("身份证号不能为空");
        }
        String normalizedDataChannelCode = normalizePullBankInfoDataChannelCode(dataChannelCode);

        if (LsfxConstants.DATA_CHANNEL_ZJRCU.equals(normalizedDataChannelCode)) {
            if (!StringUtils.hasText(startDate) || !StringUtils.hasText(endDate)) {
                throw new IllegalArgumentException("开始日期和结束日期不能为空");
            }
            LocalDate start = LocalDate.parse(startDate);
            LocalDate end = LocalDate.parse(endDate);
            if (start.isAfter(end)) {
                throw new IllegalArgumentException("开始日期不能晚于结束日期");
            }
        }

        projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据");
        projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标，暂不允许上传或拉取数据");

        CcdiProject project = projectMapper.selectById(projectId);
        if (project == null) {
            throw new IllegalArgumentException("项目不存在: projectId=" + projectId);
        }
        Integer lsfxProjectId = project.getLsfxProjectId();
        if (lsfxProjectId == null) {
            throw new IllegalStateException("项目未关联流水分析平台: projectId=" + projectId);
        }

        String batchId = UUID.randomUUID().toString().replace("-", "");
        Date now = new Date();
        List<CcdiFileUploadRecord> records = new ArrayList<>();
        List<String> normalizedIdCards = new ArrayList<>();
        for (String idCard : idCards) {
            if (!StringUtils.hasText(idCard)) {
                continue;
            }
            String normalized = idCard.trim();
            normalizedIdCards.add(normalized);

            CcdiFileUploadRecord record = new CcdiFileUploadRecord();
            record.setProjectId(projectId);
            record.setLsfxProjectId(lsfxProjectId);
            record.setFileName(normalized);
            record.setFileSize(0L);
            record.setFileStatus("uploading");
            record.setAccountNos(normalized);
            record.setUploadTime(now);
            record.setUploadUser(username);
            records.add(record);
        }
        if (records.isEmpty()) {
            throw new IllegalArgumentException("身份证号不能为空");
        }

        recordMapper.insertBatch(records);

        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                CompletableFuture.runAsync(() -> submitPullBankInfoTasks(
                    projectId, lsfxProjectId, records, normalizedIdCards,
                    normalizedDataChannelCode, startDate, endDate, batchId
                ));
            }
        });
        return batchId;
    }

    private String normalizePullBankInfoDataChannelCode(String dataChannelCode) {
        if (!StringUtils.hasText(dataChannelCode)) {
            throw new IllegalArgumentException("流水来源不能为空");
        }
        String normalized = dataChannelCode.trim().toUpperCase();
        if (!LsfxConstants.DATA_CHANNEL_ZJRCU.equals(normalized)
            && !LsfxConstants.DATA_CHANNEL_JZL.equals(normalized)) {
            throw new IllegalArgumentException("流水来源不支持");
        }
        return normalized;
    }

    @Override
    public String deleteFileUploadRecord(Long id, Long operatorUserId) {
        CcdiFileUploadRecord record = recordMapper.selectById(id);
        validateDeleteRecord(record);

        /*
         * 按当前要求，项目管理-上传数据页面删除时先不调用流水分析平台删除接口。
         * DeleteFilesRequest request = new DeleteFilesRequest();
         * request.setGroupId(record.getLsfxProjectId());
         * request.setLogIds(new Integer[]{record.getLogId()});
         * request.setUserId(toUploadUserId(operatorUserId));
         *
         * DeleteFilesResponse response = lsfxClient.deleteFiles(request);
         * if (response == null || Boolean.FALSE.equals(response.getSuccessResponse())) {
         *     throw new RuntimeException("流水分析平台删除文件失败");
         * }
         */

        bankStatementMapper.deleteByProjectIdAndBatchId(record.getProjectId(), record.getLogId());
        refreshProjectTargetCount(record.getProjectId());

        CcdiFileUploadRecord update = new CcdiFileUploadRecord();
        update.setId(record.getId());
        update.setFileStatus("deleted");
        int updatedRows = recordMapper.updateById(update);
        if (updatedRows <= 0) {
            throw new RuntimeException("更新上传记录状态失败");
        }

        bankTagService.submitAutoRebuild(record.getProjectId(), TriggerType.AUTO_FILE_DELETE);
        return "删除成功，已开始项目重新打标";
    }

    @Override
    public Page<CcdiFileUploadRecord> selectPage(Page<CcdiFileUploadRecord> page,
                                                  CcdiFileUploadQueryDTO queryDTO) {
        LambdaQueryWrapper<CcdiFileUploadRecord> queryWrapper = new LambdaQueryWrapper<>();

        // 项目ID
        if (queryDTO.getProjectId() != null) {
            queryWrapper.eq(CcdiFileUploadRecord::getProjectId, queryDTO.getProjectId());
        }

        // 文件状态
        if (StringUtils.hasText(queryDTO.getFileStatus())) {
            queryWrapper.eq(CcdiFileUploadRecord::getFileStatus, queryDTO.getFileStatus());
        }

        // 文件名称(模糊查询)
        if (StringUtils.hasText(queryDTO.getFileName())) {
            queryWrapper.like(CcdiFileUploadRecord::getFileName, queryDTO.getFileName());
        }

        // 上传人
        if (StringUtils.hasText(queryDTO.getUploadUser())) {
            queryWrapper.eq(CcdiFileUploadRecord::getUploadUser, queryDTO.getUploadUser());
        }

        // 按上传时间倒序
        queryWrapper.orderByDesc(CcdiFileUploadRecord::getUploadTime);

        return recordMapper.selectPage(page, queryWrapper);
    }

    @Override
    public CcdiFileUploadStatisticsVO countByStatus(Long projectId) {
        // 查询统计数据
        List<Map<String, Object>> statusCounts = recordMapper.countByStatus(projectId);

        // 组装 VO
        CcdiFileUploadStatisticsVO vo = new CcdiFileUploadStatisticsVO();
        vo.setUploading(0L);
        vo.setParsing(0L);
        vo.setParsedSuccess(0L);
        vo.setParsedFailed(0L);
        vo.setDeleted(0L);

        long total = 0L;
        for (Map<String, Object> item : statusCounts) {
            String status = (String) item.get("status");
            Long count = ((Number) item.get("count")).longValue();
            total += count;

            switch (status) {
                case "uploading" -> vo.setUploading(count);
                case "parsing" -> vo.setParsing(count);
                case "parsed_success" -> vo.setParsedSuccess(count);
                case "parsed_failed" -> vo.setParsedFailed(count);
                case "deleted" -> vo.setDeleted(count);
            }
        }

        vo.setTotal(total);
        return vo;
    }

    @Override
    public CcdiFileUploadRecord getById(Long id) {
        return recordMapper.selectById(id);
    }

    @Transactional
    @Override
    public String batchUploadFiles(Long projectId, MultipartFile[] files, String username) {
        log.info("【文件上传】开始批量上传: projectId={}, 文件数量={}, username={}",
                 projectId, files.length, username);

        projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据");
        projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标，暂不允许上传或拉取数据");

        // 1. 生成批次ID
        String batchId = UUID.randomUUID().toString().replace("-", "");

        // 2. 查询项目信息并获取 lsfxProjectId
        CcdiProject project = projectMapper.selectById(projectId);
        if (project == null) {
            throw new IllegalArgumentException("项目不存在: projectId=" + projectId);
        }

        Integer lsfxProjectId = project.getLsfxProjectId();
        if (lsfxProjectId == null) {
            throw new IllegalStateException("项目未关联流水分析平台: projectId=" + projectId);
        }

        log.info("【文件上传】项目信息验证通过: projectId={}, lsfxProjectId={}", projectId, lsfxProjectId);

        List<String> normalizedFileNames = normalizeAndValidateUploadFileNames(files);

        // Critical Fix #2 & #4: 保存临时文件和创建记录在同一个循环中，确保一一对应
        List<String> tempFilePaths = new ArrayList<>();
        List<CcdiFileUploadRecord> records = new ArrayList<>();
        Date now = new Date();

        try {
            // 确保临时目录存在
            Path tempDir = Paths.get(getTempFileDir());
            if (!Files.exists(tempDir)) {
                Files.createDirectories(tempDir);
            }

            // 同一个循环中保存临时文件和创建记录，确保索引一一对应
            for (int i = 0; i < files.length; i++) {
                MultipartFile file = files[i];
                String normalizedFileName = normalizedFileNames.get(i);

                // 1. 保存临时文件
                String originalFilename = file.getOriginalFilename();
                String tempFileName = batchId + "_" + i + "_" + System.currentTimeMillis() + "_" + normalizedFileName;
                Path tempFilePath = tempDir.resolve(tempFileName);

                Files.copy(file.getInputStream(), tempFilePath, StandardCopyOption.REPLACE_EXISTING);
                tempFilePaths.add(tempFilePath.toString());

                log.debug("【文件上传】保存临时文件[{}]: originalName={}, tempPath={}",
                         i, originalFilename, tempFilePath);

                // 2. 创建记录（使用相同的索引i）
                CcdiFileUploadRecord record = new CcdiFileUploadRecord();
                record.setProjectId(projectId);
                record.setLsfxProjectId(lsfxProjectId);
                record.setFileName(normalizedFileName);
                record.setFileSize(file.getSize());
                record.setFileStatus("uploading");
                record.setUploadTime(now);
                record.setUploadUser(username);
                records.add(record);
            }
        } catch (IOException e) {
            log.error("【文件上传】保存临时文件失败", e);
            throw new RuntimeException("保存临时文件失败: " + e.getMessage(), e);
        }

        // 验证数量一致性
        if (tempFilePaths.size() != records.size()) {
            throw new RuntimeException(String.format(
                "临时文件数量(%d)与记录数量(%d)不一致", tempFilePaths.size(), records.size()));
        }

        recordMapper.insertBatch(records);
        log.info("【文件上传】批量插入记录成功: 数量={}", records.size());

        // Critical Fix #3: 验证ID已生成
        for (CcdiFileUploadRecord record : records) {
            if (record.getId() == null) {
                throw new RuntimeException("批量插入失败: 未生成记录ID,请检查Mapper配置useGeneratedKeys=true");
            }
        }
        log.debug("【文件上传】ID验证通过: 所有记录ID已生成");

        // Critical Fix #1: 使用TransactionSynchronization确保异步任务在事务提交后启动
        final Integer finalLsfxProjectId = lsfxProjectId;
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                log.info("【文件上传】事务已提交,启动异步任务");
                CompletableFuture.runAsync(() -> {
                    submitTasksAsync(projectId, finalLsfxProjectId, tempFilePaths, records, batchId);
                });
            }
        });

        log.info("【文件上传】批量上传任务已提交: batchId={}", batchId);
        return batchId;
    }

    private List<String> normalizeAndValidateUploadFileNames(MultipartFile[] files) {
        List<String> normalizedFileNames = new ArrayList<>();
        for (MultipartFile file : files) {
            String originalFileName = file.getOriginalFilename() == null ? "" : file.getOriginalFilename();
            String normalizedFileName = FILE_NAME_SPACE_PATTERN.matcher(originalFileName).replaceAll("");
            validateUploadFileName(normalizedFileName);
            normalizedFileNames.add(normalizedFileName);
        }
        return normalizedFileNames;
    }

    private void validateUploadFileName(String fileName) {
        if (!StringUtils.hasText(fileName)) {
            throw new IllegalArgumentException("文件名不能为空");
        }

        String lowerFileName = fileName.toLowerCase(Locale.ROOT);
        if (!lowerFileName.endsWith(".xlsx") && !lowerFileName.endsWith(".csv")
            && !lowerFileName.endsWith(".pdf")) {
            throw new IllegalArgumentException("文件 " + fileName + " 格式不支持, 仅支持 PDF, CSV, XLSX 文件");
        }

        String mainFileName = getMainFileName(fileName);
        if (!UPLOAD_FILE_NAME_ID_CARD_PATTERN.matcher(mainFileName).find()) {
            throw new IllegalArgumentException("文件 " + fileName + " 文件名未包含身份证信息");
        }
    }

    private String getMainFileName(String fileName) {
        int extensionIndex = fileName.lastIndexOf('.');
        if (extensionIndex <= 0) {
            return fileName;
        }
        return fileName.substring(0, extensionIndex);
    }

    /**
     * 调度线程:循环提交任务到线程池
     * 支持等待30秒重试机制
     *
     * @param projectId 项目ID
     * @param lsfxProjectId 流水分析项目ID
     * @param tempFilePaths 临时文件路径列表
     * @param records 文件上传记录列表
     * @param batchId 批次ID
     */
    private void submitTasksAsync(Long projectId, Integer lsfxProjectId,
                                   List<String> tempFilePaths,
                                   List<CcdiFileUploadRecord> records,
                                   String batchId) {
        log.info("【文件上传】调度线程启动: projectId={}, batchId={}", projectId, batchId);

        List<CompletableFuture<Boolean>> futures = new ArrayList<>();

        // 循环提交任务
        for (int i = 0; i < tempFilePaths.size(); i++) {
                // Critical Fix #6: 检查线程中断状态
                if (Thread.currentThread().isInterrupted()) {
                    log.warn("【文件上传】调度线程被中断,停止提交剩余任务");
                    break;
                }

                String tempFilePath = tempFilePaths.get(i);
                CcdiFileUploadRecord record = records.get(i);

                boolean submitted = false;
                int retryCount = 0;

                while (!submitted && retryCount < 2) {
                    try {
                        // 尝试提交异步任务
                        CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(
                            () -> processFileAsync(projectId, lsfxProjectId, tempFilePath, record.getId(), batchId, record),
                            fileUploadExecutor
                        );
                        futures.add(future);
                        submitted = true;
                        log.info("【文件上传】任务提交成功: fileName={}, recordId={}",
                                 record.getFileName(), record.getId());
                    } catch (RejectedExecutionException e) {
                        retryCount++;
                        if (retryCount == 1) {
                            log.warn("【文件上传】线程池已满,等待30秒后重试: fileName={}",
                                     record.getFileName());
                            try {
                                Thread.sleep(30000);
                            } catch (InterruptedException ie) {
                                Thread.currentThread().interrupt();
                                log.error("【文件上传】等待被中断: fileName={}", record.getFileName());
                                updateRecordStatus(record.getId(), "parsed_failed", "任务提交被中断");
                                futures.add(CompletableFuture.completedFuture(Boolean.FALSE));
                                break;
                            }
                        } else {
                            log.error("【文件上传】重试失败,放弃任务: fileName={}", record.getFileName());
                            updateRecordStatus(record.getId(), "parsed_failed", "系统繁忙,请稍后重试");
                            futures.add(CompletableFuture.completedFuture(Boolean.FALSE));
                        }
                    }
                }
            }

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
            .whenComplete((unused, throwable) -> {
                boolean anySuccess = futures.stream().anyMatch(future -> Boolean.TRUE.equals(future.getNow(Boolean.FALSE)));
                handleTagRebuildAfterBatchCompletion(projectId, TriggerType.AUTO_BATCH_UPLOAD, anySuccess);
            });

        log.info("【文件上传】调度线程完成: projectId={}, batchId={}", projectId, batchId);
    }

    /**
     * 更新记录状态(辅助方法)
     */
    private void updateRecordStatus(Long recordId, String status, String errorMessage) {
        CcdiFileUploadRecord record = new CcdiFileUploadRecord();
        record.setId(recordId);
        record.setFileStatus(status);
        record.setErrorMessage(normalizeErrorMessage(errorMessage));
        recordMapper.updateById(record);
    }

    private void updateFailedRecord(CcdiFileUploadRecord record, String errorMessage) {
        record.setFileStatus("parsed_failed");
        record.setErrorMessage(normalizeErrorMessage(errorMessage));
        recordMapper.updateById(record);
    }

    private String normalizeErrorMessage(String errorMessage) {
        if (!StringUtils.hasText(errorMessage)) {
            return null;
        }
        if (errorMessage.length() <= MAX_ERROR_MESSAGE_LENGTH) {
            return errorMessage;
        }
        return errorMessage.substring(0, MAX_ERROR_MESSAGE_LENGTH);
    }

    private String trimAccountNo(String value) {
        return value == null ? null : value.trim();
    }

    private void normalizeDedupFields(CcdiBankStatement statement) {
        statement.setLeAccountNo(trimAccountNo(statement.getLeAccountNo()));
    }

    private void submitPullBankInfoTasks(Long projectId,
                                         Integer lsfxProjectId,
                                         List<CcdiFileUploadRecord> records,
                                         List<String> idCards,
                                         String dataChannelCode,
                                         String startDate,
                                         String endDate,
                                         String batchId) {
        log.info("【拉取本行信息】调度线程启动: projectId={}, batchId={}", projectId, batchId);

        List<CompletableFuture<Boolean>> futures = new ArrayList<>();

        for (int i = 0; i < records.size(); i++) {
            if (Thread.currentThread().isInterrupted()) {
                log.warn("【拉取本行信息】调度线程被中断,停止提交剩余任务");
                break;
            }

            CcdiFileUploadRecord record = records.get(i);
            String idCard = idCards.get(i);
            boolean submitted = false;
            int retryCount = 0;

            while (!submitted && retryCount < 2) {
                try {
                    CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(
                        () -> processPullBankInfoAsync(projectId, lsfxProjectId, record, idCard, dataChannelCode, startDate, endDate),
                        fileUploadExecutor
                    );
                    futures.add(future);
                    submitted = true;
                    log.info("【拉取本行信息】任务提交成功: idCard={}, recordId={}", idCard, record.getId());
                } catch (RejectedExecutionException e) {
                    retryCount++;
                    if (retryCount == 1) {
                        log.warn("【拉取本行信息】线程池已满,等待30秒后重试: idCard={}", idCard);
                        try {
                            Thread.sleep(30000);
                        } catch (InterruptedException interruptedException) {
                            Thread.currentThread().interrupt();
                            log.error("【拉取本行信息】等待被中断: idCard={}", idCard);
                            updateRecordStatus(record.getId(), "parsed_failed", "任务提交被中断");
                            futures.add(CompletableFuture.completedFuture(Boolean.FALSE));
                            break;
                        }
                    } else {
                        log.error("【拉取本行信息】重试失败,放弃任务: idCard={}", idCard);
                        updateRecordStatus(record.getId(), "parsed_failed", "系统繁忙,请稍后重试");
                        futures.add(CompletableFuture.completedFuture(Boolean.FALSE));
                    }
                }
            }
        }

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
            .whenComplete((unused, throwable) -> {
                boolean anySuccess = futures.stream().anyMatch(future -> Boolean.TRUE.equals(future.getNow(Boolean.FALSE)));
                handleTagRebuildAfterBatchCompletion(projectId, TriggerType.AUTO_PULL_BANK_INFO, anySuccess);
            });
    }

    public boolean processPullBankInfoAsync(Long projectId,
                                            Integer lsfxProjectId,
                                            CcdiFileUploadRecord record,
                                            String idCard,
                                            String dataChannelCode,
                                            String startDate,
                                            String endDate ) {
        try {
            String normalizedDataChannelCode = normalizePullBankInfoDataChannelCode(dataChannelCode);
            FetchInnerFlowRequest request = new FetchInnerFlowRequest();
            request.setGroupId(lsfxProjectId);
            request.setCustomerNo(idCard);
            request.setDataChannelCode(normalizedDataChannelCode);
            request.setRequestDateId(Integer.parseInt(LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)));
            if (LsfxConstants.DATA_CHANNEL_JZL.equals(normalizedDataChannelCode)) {
                request.setDataStartDateId(0);
                request.setDataEndDateId(0);
            } else {
                request.setDataStartDateId(Integer.parseInt(startDate.replace("-", "")));
                request.setDataEndDateId(Integer.parseInt(endDate.replace("-", "")));
            }
            request.setUploadUserId(LsfxConstants.DEFAULT_USER_ID);

            FetchInnerFlowResponse response = lsfxClient.fetchInnerFlow(request);
            if (response == null || response.getData() == null || response.getData().isEmpty()) {
                throw new RuntimeException("拉取本行信息失败: 未返回logId");
            }

            Integer logId = response.getData().get(0);
            if (logId == null) {
                throw new RuntimeException("拉取本行信息失败: 未返回logId");
            }

            processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId);
            return true;
        } catch (Exception e) {
            log.error("【拉取本行信息】处理失败: idCard={}, recordId={}", idCard, record.getId(), e);
            updateFailedRecord(record, e.getMessage());
            return false;
        }
    }

    /**
     * 异步处理单个文件的完整流程
     * 包含:上传 → 轮询解析状态 → 获取结果 → 保存流水数据
     *
     * @param projectId 项目ID
     * @param lsfxProjectId 流水分析项目ID
     * @param tempFilePath 临时文件路径
     * @param recordId 记录ID
     * @param batchId 批次ID
     * @param record 文件上传记录
     */
    @Async("fileUploadExecutor")
    public boolean processFileAsync(Long projectId, Integer lsfxProjectId, String tempFilePath,
                                    Long recordId, String batchId, CcdiFileUploadRecord record) {
        log.info("【文件上传】开始处理文件: fileName={}, recordId={}, tempPath={}",
                 record.getFileName(), recordId, tempFilePath);

        try {
            // 步骤1:状态已是uploading,记录已存在

            // 从临时文件路径读取文件
            Path filePath = Paths.get(tempFilePath);
            if (!Files.exists(filePath)) {
                throw new RuntimeException("临时文件不存在: " + tempFilePath);
            }

            // 步骤2:上传文件到流水分析平台
            log.info("【文件上传】步骤2: 上传文件到流水分析平台, tempPath={}", tempFilePath);

            File file = filePath.toFile();
            if (!file.exists()) {
                throw new RuntimeException("临时文件不存在: " + tempFilePath);
            }

            UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file, record.getFileName());
            if (uploadResponse == null || uploadResponse.getData() == null
                || uploadResponse.getData().getUploadLogList() == null
                || uploadResponse.getData().getUploadLogList().isEmpty()) {
                throw new RuntimeException("上传文件失败: 响应数据为空");
            }

            // 从 uploadLogList 中获取第一个 logId
            Integer logId = uploadResponse.getData().getUploadLogList().get(0).getLogId();
            if (logId == null) {
                throw new RuntimeException("上传文件失败: 未返回logId");
            }

            log.info("【文件上传】文件上传成功: logId={}", logId);
            processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, true);
            log.info("【文件上传】处理完成: fileName={}", record.getFileName());
            return true;

        } catch (Exception e) {
            log.error("【文件上传】处理失败: fileName={}", record.getFileName(), e);
            updateRecordStatus(recordId, "parsed_failed", e.getMessage());
            return false;
        } finally {
            // 清理临时文件
            try {
                Path filePath = Paths.get(tempFilePath);
                if (Files.exists(filePath)) {
                    Files.delete(filePath);
                    log.debug("【文件上传】清理临时文件: {}", tempFilePath);
                }
            } catch (IOException e) {
                log.warn("【文件上传】清理临时文件失败: {}", tempFilePath, e);
            }
        }
    }

    private void handleTagRebuildAfterBatchCompletion(Long projectId, TriggerType triggerType, Boolean anySuccess) {
        log.info("【流水标签】批处理完成，准备触发自动重算: projectId={}, triggerType={}, anySuccess={}",
            projectId, triggerType, anySuccess);
        if (!Boolean.TRUE.equals(anySuccess)) {
            log.warn("【流水标签】跳过自动重算: projectId={}, triggerType={}, reason=all_records_failed",
                projectId, triggerType);
            return;
        }
        bankTagService.submitAutoRebuild(projectId, triggerType);
    }

    private void processRecordAfterLogIdReady(Long projectId,
                                              Integer lsfxProjectId,
                                              CcdiFileUploadRecord record,
                                              Integer logId) {
        processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, false);
    }

    private void processRecordAfterLogIdReady(Long projectId,
                                              Integer lsfxProjectId,
                                              CcdiFileUploadRecord record,
                                              Integer logId,
                                              boolean preserveRecordFileName) {
        log.info("【文件上传】步骤3: 更新状态为解析中, logId={}", logId);
        record.setLogId(logId);
        record.setFileStatus("parsing");
        recordMapper.updateById(record);

        log.info("【文件上传】步骤4: 开始轮询解析状态");
        boolean parsingComplete = waitForParsingComplete(lsfxProjectId, logId.toString());
        if (!parsingComplete) {
            throw new RuntimeException("解析超时(超过10分钟),请检查文件格式是否正确");
        }

        log.info("【文件上传】步骤5: 获取文件上传状态: logId={}", logId);
        GetFileUploadStatusRequest statusRequest = new GetFileUploadStatusRequest();
        statusRequest.setGroupId(lsfxProjectId);
        statusRequest.setLogId(logId);

        GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(statusRequest);
        if (statusResponse == null || statusResponse.getData() == null
            || statusResponse.getData().getLogs() == null
            || statusResponse.getData().getLogs().isEmpty()) {
            throw new RuntimeException("获取文件上传状态失败: 响应数据为空");
        }

        GetFileUploadStatusResponse.LogItem logItem = statusResponse.getData().getLogs().get(0);
        Integer status = logItem.getStatus();
        String uploadStatusDesc = logItem.getUploadStatusDesc();
        if (!preserveRecordFileName) {
            String fileName = StringUtils.hasText(logItem.getUploadFileName())
                ? logItem.getUploadFileName()
                : logItem.getDownloadFileName();
            if (StringUtils.hasText(fileName)) {
                record.setFileName(fileName);
            }
        }
        if (logItem.getFileSize() != null) {
            record.setFileSize(logItem.getFileSize());
        }

        log.info("【文件上传】文件状态: status={}, uploadStatusDesc={}", status, uploadStatusDesc);
        boolean parseSuccess = status != null && status == -5
            && "data.wait.confirm.newaccount".equals(uploadStatusDesc);
        if (!parseSuccess) {
            log.warn("【文件上传】步骤6: 解析失败: status={}, desc={}", status, uploadStatusDesc);
            updateFailedRecord(record, "解析失败: " + uploadStatusDesc);
            return;
        }

        log.info("【文件上传】步骤6: 解析成功,保存主体信息");
        List<String> enterpriseNames = logItem.getEnterpriseNameList();
        List<String> accountNos = logItem.getAccountNoList();
        String enterpriseNamesStr = enterpriseNames != null ? String.join(",", enterpriseNames) : "";
        String accountNosStr = accountNos != null ? String.join(",", accountNos) : "";
        log.info("【文件上传】主体信息已保存: enterpriseNames={}, accountNos={}",
            enterpriseNamesStr, accountNosStr);

        log.info("【文件上传】步骤7: 获取流水数据");
        String fallbackCretNo = extractIdCardFromFileName(record.getFileName());
        FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId,
            fallbackCretNo);
        if (!fetchResult.isSuccess()) {
            updateFailedRecord(record, fetchResult.getErrorMessage());
            return;
        }

        record.setFileStatus("parsed_success");
        record.setEnterpriseNames(enterpriseNamesStr);
        record.setAccountNos(accountNosStr);
        record.setErrorMessage(null);
        recordMapper.updateById(record);
    }

    /**
     * 轮询解析状态（固定间隔2秒，最多300次）
     *
     * @param groupId 项目ID
     * @param logId 文件ID
     * @return true=解析完成，false=超时未完成
     */
    private boolean waitForParsingComplete(Integer groupId, String logId) {
        log.info("【文件上传】开始轮询解析状态: groupId={}, logId={}", groupId, logId);

        int maxRetries = 300;
        int intervalSeconds = 2;

        for (int i = 1; i <= maxRetries; i++) {
            try {
                // 调用检查解析状态接口
                CheckParseStatusResponse response = lsfxClient.checkParseStatus(groupId, logId);

                if (response == null || response.getData() == null) {
                    log.warn("【文件上传】轮询第{}次: 响应数据为空", i);
                    Thread.sleep(intervalSeconds * 1000L);
                    continue;
                }

                Boolean parsing = response.getData().getParsing();
                log.debug("【文件上传】轮询第{}次: parsing={}", i, parsing);

                // parsing=false 表示解析完成
                if (Boolean.FALSE.equals(parsing)) {
                    log.info("【文件上传】解析完成: logId={}, 轮询次数={}", logId, i);
                    return true;
                }

                // 未完成，等待后继续
                if (i < maxRetries) {
                    Thread.sleep(intervalSeconds * 1000L);
                }

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("【文件上传】轮询被中断: logId={}", logId, e);
                return false;
            } catch (Exception e) {
                log.error("【文件上传】轮询异常: logId={}, 次数={}", logId, i, e);
                // 继续轮询，不中断
            }
        }

        log.warn("【文件上传】轮询超时: logId={}, 已轮询{}次", logId, maxRetries);
        return false;
    }

    /**
     * 获取并保存流水数据（每页1000条，批量插入每批1000条）
     *
     * @param projectId 项目ID（业务字段）
     * @param groupId 流水分析平台项目ID
     * @param logId 文件ID
     */
    private FetchBankStatementResult fetchAndSaveBankStatements(Long projectId, Integer groupId,
                                                                Integer logId,
                                                                String fallbackCretNo) {
        log.info("【文件上传】开始获取流水数据: projectId={}, groupId={}, logId={}",
                 projectId, groupId, logId);

        FetchBankStatementResult result = new FetchBankStatementResult();

        try {
            GetBankStatementRequest firstRequest = new GetBankStatementRequest();
            firstRequest.setGroupId(groupId);
            firstRequest.setLogId(logId);
            firstRequest.setPageNow(1);
            firstRequest.setPageSize(1);

            GetBankStatementResponse firstResponse = lsfxClient.getBankStatement(firstRequest);
            if (firstResponse == null || firstResponse.getData() == null) {
                result.setSuccess(false);
                result.setErrorMessage("获取流水数据失败: 响应数据为空");
                cleanupBankStatements(projectId, logId);
                return result;
            }

            Integer totalCount = firstResponse.getData().getTotalCount();
            result.setTotalCount(totalCount == null ? 0 : totalCount);
            if (totalCount == null || totalCount <= 0) {
                log.warn("【文件上传】无流水数据需要保存: totalCount={}", totalCount);
                refreshProjectTargetCount(projectId);
                result.setSuccess(true);
                return result;
            }

            int pageSize = 1000;
            int batchSize = 1000;
            int totalPages = (int) Math.ceil((double) totalCount / pageSize);
            int totalAttempted = 0;
            List<CcdiBankStatement> batchList = new ArrayList<>(batchSize);

            log.info("【文件上传】获取到总数: totalCount={}", totalCount);
            log.info("【文件上传】分页信息: 每页{}条, 共{}页", pageSize, totalPages);

            for (int pageNow = 1; pageNow <= totalPages; pageNow++) {
                try {
                    GetBankStatementRequest request = new GetBankStatementRequest();
                    request.setGroupId(groupId);
                    request.setLogId(logId);
                    request.setPageNow(pageNow);
                    request.setPageSize(pageSize);

                    GetBankStatementResponse response = lsfxClient.getBankStatement(request);
                    if (response == null || response.getData() == null
                        || response.getData().getBankStatementList() == null) {
                        result.setSuccess(false);
                        result.setErrorMessage("获取流水数据失败: 第 " + pageNow + " 页响应异常");
                        cleanupBankStatements(projectId, logId);
                        return result;
                    }

                    List<GetBankStatementResponse.BankStatementItem> items =
                        response.getData().getBankStatementList();
                    log.debug("【文件上传】获取第{}页数据: {}条", pageNow, items.size());

                    for (GetBankStatementResponse.BankStatementItem item : items) {
                        CcdiBankStatement statement = CcdiBankStatement.fromResponse(item);
                        if (statement != null) {
                            statement.setBatchId(logId);
                            statement.setProjectId(projectId);
                            fillMissingCretNo(statement, fallbackCretNo);
                            normalizeDedupFields(statement);
                            batchList.add(statement);

                            if (batchList.size() >= batchSize) {
                                int currentBatchSize = batchList.size();
                                bankStatementMapper.insertBatch(batchList);
                                totalAttempted += currentBatchSize;
                                log.debug("【文件上传】批量写入流水 {}条", currentBatchSize);
                                batchList.clear();
                            }
                        }
                    }
                } catch (Exception e) {
                    log.error("【文件上传】获取或保存流水数据失败: pageNow={}", pageNow, e);
                    result.setSuccess(false);
                    result.setErrorMessage("获取或保存流水数据失败: " + e.getMessage());
                    cleanupBankStatements(projectId, logId);
                    return result;
                }
            }

            if (!batchList.isEmpty()) {
                int currentBatchSize = batchList.size();
                bankStatementMapper.insertBatch(batchList);
                totalAttempted += currentBatchSize;
                log.debug("【文件上传】批量插入剩余流水 {}条", batchList.size());
            }

            log.info("【文件上传】流水入库完成: fetchedCount={}, attemptedCount={}",
                totalCount, totalAttempted);
            refreshProjectTargetCount(projectId);
            result.setSuccess(true);
            result.setAttemptedCount(totalAttempted);
            return result;
        } catch (Exception e) {
            log.error("【文件上传】获取或保存流水数据失败: logId={}", logId, e);
            result.setSuccess(false);
            result.setErrorMessage("获取或保存流水数据失败: " + e.getMessage());
            cleanupBankStatements(projectId, logId);
            return result;
        }
    }

    private String extractIdCardFromFileName(String fileName) {
        if (!StringUtils.hasText(fileName)) {
            return null;
        }
        String mainFileName = getMainFileName(fileName);
        Matcher matcher = UPLOAD_FILE_NAME_ID_CARD_PATTERN.matcher(mainFileName);
        return matcher.find() ? matcher.group() : null;
    }

    private void fillMissingCretNo(CcdiBankStatement statement, String fallbackCretNo) {
        if (statement == null || StringUtils.hasText(statement.getCretNo()) || !StringUtils.hasText(fallbackCretNo)) {
            return;
        }
        statement.setCretNo(fallbackCretNo);
    }

    private void cleanupBankStatements(Long projectId, Integer logId) {
        bankStatementMapper.deleteByProjectIdAndBatchId(projectId, logId);
    }

    private void refreshProjectTargetCount(Long projectId) {
        CcdiProject project = projectMapper.selectById(projectId);
        if (project == null) {
            log.warn("【项目】刷新目标人数时项目不存在: projectId={}", projectId);
            return;
        }

        int targetCount = bankStatementMapper.countMatchedStaffCountByProjectId(projectId);
        project.setTargetCount(targetCount);
        projectMapper.updateById(project);
    }

    private void validateDeleteRecord(CcdiFileUploadRecord record) {
        if (record == null) {
            throw new RuntimeException("上传记录不存在");
        }
        if ("HISTORY_IMPORT".equals(record.getSourceType())) {
            throw new ServiceException("历史导入文件不支持删除");
        }
        if (!"parsed_success".equals(record.getFileStatus())) {
            if ("deleted".equals(record.getFileStatus())) {
                throw new RuntimeException("文件已删除，请勿重复操作");
            }
            throw new RuntimeException("仅支持删除解析成功文件");
        }
        if (record.getLsfxProjectId() == null) {
            throw new RuntimeException("缺少流水分析项目ID");
        }
        if (record.getLogId() == null) {
            throw new RuntimeException("缺少文件logId");
        }
    }

    private Integer toUploadUserId(Long userId) {
        if (userId == null) {
            throw new IllegalArgumentException("当前登录用户ID不能为空");
        }
        return Math.toIntExact(userId);
    }
}
