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

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiModelParam;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalPersonQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectUnsubmittedAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisAbnormalDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisAbnormalGroupVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportParamVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountFlowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountSubjectVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountVO;
import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectRiskExclusionMapper;
import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 结果总览服务实现
 */
@Service
public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewService {

    private static final String ACTION_LABEL = "查看详情";

    private static final String SUSPICIOUS_TYPE_ALL = "ALL";

    private static final String SUSPICIOUS_TYPE_MODEL_RULE = "MODEL_RULE";

    private static final String SUSPICIOUS_TYPE_EXTERNAL_PERSON = "EXTERNAL_PERSON";

    private static final String EXCLUSION_TYPE_STATEMENT = "STATEMENT";

    private static final String EXCLUSION_TYPE_OBJECT = "OBJECT";

    @Resource
    private CcdiProjectOverviewMapper overviewMapper;

    @Resource
    private CcdiProjectMapper projectMapper;

    @Resource
    private CcdiBankStatementMapper bankStatementMapper;

    @Resource
    private CcdiModelParamMapper modelParamMapper;

    @Resource
    private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;

    @Resource
    private CcdiBankTagResultMapper bankTagResultMapper;

    @Resource
    private CcdiProjectRiskExclusionMapper riskExclusionMapper;

    @Resource
    private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;

    @Resource
    private CcdiProjectRiskDetailWorkbookExporter workbookExporter;

    @Resource
    private CcdiProjectOverviewReportPdfExporter reportPdfExporter;

    @Override
    public CcdiProjectOverviewDashboardVO getDashboard(Long projectId) {
        CcdiProject project = overviewMapper.selectDashboardBaseByProjectId(projectId);
        if (project == null) {
            throw new ServiceException("项目不存在");
        }

        int targetCount = defaultZero(project.getTargetCount());
        int highRiskCount = defaultZero(project.getHighRiskCount());
        int mediumRiskCount = defaultZero(project.getMediumRiskCount());
        int lowRiskCount = defaultZero(project.getLowRiskCount());
        int noRiskCount = targetCount - highRiskCount - mediumRiskCount - lowRiskCount;

        CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO();
        dashboard.setTitle("风险仪表盘");
        dashboard.setSubtitle("风险仪表盘数据概览");
        dashboard.setStats(List.of(
            buildStat("people", "总人数", targetCount),
            buildStat("riskPeople", "高风险", highRiskCount),
            buildStat("medium", "中风险", mediumRiskCount),
            buildStat("low", "低风险", lowRiskCount),
            buildStat("count", "无风险人员", noRiskCount)
        ));
        return dashboard;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void excludeRisk(CcdiProjectRiskExclusionSaveDTO dto) {
        CcdiProjectRiskExclusion exclusion = buildRiskExclusion(dto);
        CcdiProject project = getRequiredProject(exclusion.getProjectId());
        if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus()) || Integer.valueOf(1).equals(project.getIsArchived())) {
            throw new ServiceException("已归档项目暂不允许排除可疑");
        }
        if (CcdiProjectStatusConstants.TAGGING.equals(project.getStatus())) {
            throw new ServiceException("项目正在打标中，暂不允许排除可疑");
        }
        if (riskExclusionMapper.countExistingRiskHit(exclusion) <= 0) {
            throw new ServiceException("未找到可排除的预警命中");
        }
        riskExclusionMapper.upsertExclusion(exclusion);
        refreshOverviewEmployeeResults(exclusion.getProjectId(), exclusion.getUpdateBy());
    }

    @Override
    public CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(CcdiProjectRiskPeopleQueryDTO queryDTO) {
        Long projectId = queryDTO.getProjectId();
        ensureProjectExists(projectId);

        Page<CcdiProjectEmployeeRiskAggregateVO> page = new Page<>(
            defaultRiskPeoplePageNum(queryDTO.getPageNum()),
            defaultRiskPeoplePageSize(queryDTO.getPageSize())
        );
        Page<CcdiProjectEmployeeRiskAggregateVO> resultPage = overviewMapper.selectRiskPeopleOverviewPage(page, queryDTO);

        List<CcdiProjectRiskPeopleOverviewItemVO> rows = defaultList(resultPage == null ? null : resultPage.getRecords())
            .stream()
            .map(aggregate -> buildRiskPeopleItem(projectId, aggregate))
            .toList();

        CcdiProjectRiskPeopleOverviewVO overview = new CcdiProjectRiskPeopleOverviewVO();
        overview.setRows(rows);
        overview.setTotal(resultPage == null ? 0L : resultPage.getTotal());
        overview.setPageNum(page.getCurrent());
        overview.setPageSize(page.getSize());
        return overview;
    }

    @Override
    public CcdiProjectTopRiskPeopleVO getTopRiskPeople(Long projectId) {
        ensureProjectExists(projectId);

        List<CcdiProjectTopRiskPeopleItemVO> topRiskList = overviewMapper.selectTopRiskPeopleByProjectId(projectId)
            .stream()
            .map(this::buildTopRiskPeopleItem)
            .toList();

        CcdiProjectTopRiskPeopleVO topRiskPeople = new CcdiProjectTopRiskPeopleVO();
        topRiskPeople.setTopRiskList(topRiskList);
        return topRiskPeople;
    }

    @Override
    public CcdiProjectPersonAnalysisDetailVO getPersonAnalysisDetail(CcdiProjectPersonAnalysisDetailQueryDTO queryDTO) {
        ensureProjectExists(queryDTO.getProjectId());

        CcdiProjectPersonAnalysisBasicInfoVO basicInfo = overviewMapper.selectPersonAnalysisBasicInfo(
            queryDTO.getProjectId(),
            queryDTO.getStaffIdCard()
        );
        List<CcdiBankStatementListVO> statementRows = defaultList(overviewMapper.selectPersonAnalysisStatementRows(
            queryDTO.getProjectId(),
            queryDTO.getStaffIdCard()
        ));
        List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows = defaultList(overviewMapper.selectPersonAnalysisObjectRows(
            queryDTO.getProjectId(),
            queryDTO.getStaffIdCard()
        ));
        attachStatementHitTags(statementRows, queryDTO.getProjectId());
        statementRows = statementRows.stream()
            .filter(row -> row.getHitTags() != null && !row.getHitTags().isEmpty())
            .toList();
        normalizeObjectRows(objectRows);

        CcdiProjectPersonAnalysisDetailVO detail = new CcdiProjectPersonAnalysisDetailVO();
        detail.setBasicInfo(basicInfo == null ? new CcdiProjectPersonAnalysisBasicInfoVO() : basicInfo);
        detail.setAbnormalDetail(buildAbnormalDetail(statementRows, objectRows));
        return detail;
    }

    @Override
    public CcdiProjectRiskModelCardsVO getRiskModelCards(Long projectId) {
        ensureProjectExists(projectId);

        CcdiProjectRiskModelCardsVO cards = new CcdiProjectRiskModelCardsVO();
        cards.setCardList(defaultList(overviewMapper.selectRiskModelCardsByProjectId(projectId)));
        return cards;
    }

    @Override
    public CcdiProjectRiskModelPeopleVO getRiskModelPeople(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
        ensureProjectExists(queryDTO.getProjectId());
        normalizeRiskModelPeopleQuery(queryDTO);

        Page<CcdiProjectRiskModelPeopleItemVO> page = new Page<>(
            defaultPageNum(queryDTO.getPageNum()),
            defaultPageSize(queryDTO.getPageSize())
        );
        Page<CcdiProjectRiskModelPeopleItemVO> resultPage = overviewMapper.selectRiskModelPeoplePage(page, queryDTO);

        List<CcdiProjectRiskModelPeopleItemVO> rows = defaultList(resultPage == null ? null : resultPage.getRecords())
            .stream()
            .peek(item -> item.setActionLabel(ACTION_LABEL))
            .toList();

        CcdiProjectRiskModelPeopleVO people = new CcdiProjectRiskModelPeopleVO();
        people.setRows(rows);
        people.setTotal(resultPage == null ? 0L : resultPage.getTotal());
        return people;
    }

    @Override
    public List<CcdiProjectRiskModelPeopleExcel> exportRiskModelPeople(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
        ensureProjectExists(queryDTO.getProjectId());
        normalizeRiskModelPeopleQuery(queryDTO);

        return defaultList(overviewMapper.selectRiskModelPeopleList(queryDTO)).stream()
            .map(item -> buildRiskModelPeopleExcelRow(item, "员工"))
            .toList();
    }

    @Override
    public CcdiProjectExternalPersonWarningVO getExternalPersonWarnings(CcdiProjectExternalPersonQueryDTO queryDTO) {
        ensureProjectExists(queryDTO.getProjectId());

        Page<CcdiProjectExternalPersonWarningItemVO> page = new Page<>(
            defaultRiskPeoplePageNum(queryDTO.getPageNum()),
            defaultRiskPeoplePageSize(queryDTO.getPageSize())
        );
        Page<CcdiProjectExternalPersonWarningItemVO> resultPage =
            overviewMapper.selectExternalPersonWarningPage(page, queryDTO);

        List<CcdiProjectExternalPersonWarningItemVO> rows =
            defaultList(resultPage == null ? null : resultPage.getRecords()).stream()
                .peek(item -> item.setActionLabel(ACTION_LABEL))
                .toList();

        CcdiProjectExternalPersonWarningVO warnings = new CcdiProjectExternalPersonWarningVO();
        warnings.setRows(rows);
        warnings.setTotal(resultPage == null ? 0L : resultPage.getTotal());
        warnings.setPageNum(page.getCurrent());
        warnings.setPageSize(page.getSize());
        return warnings;
    }

    @Override
    public CcdiProjectExternalRiskSummaryVO getExternalRiskSummary(Long projectId) {
        ensureProjectExists(projectId);
        CcdiProjectExternalRiskSummaryVO summary = overviewMapper.selectExternalRiskSummaryByProjectId(projectId);
        if (summary == null) {
            return new CcdiProjectExternalRiskSummaryVO();
        }
        summary.setTotal(defaultZero(summary.getTotal()));
        summary.setHigh(defaultZero(summary.getHigh()));
        summary.setMedium(defaultZero(summary.getMedium()));
        summary.setLow(defaultZero(summary.getLow()));
        summary.setNoRisk(defaultZero(summary.getNoRisk()));
        return summary;
    }

    @Override
    public List<CcdiProjectExternalPersonWarningExcel> exportExternalPersonWarnings(Long projectId) {
        ensureProjectExists(projectId);

        return defaultList(overviewMapper.selectExternalPersonWarningList(projectId)).stream()
            .map(this::buildExternalPersonWarningExcelRow)
            .toList();
    }

    @Override
    public CcdiProjectRiskModelCardsVO getExternalRiskModelCards(Long projectId) {
        ensureProjectExists(projectId);

        CcdiProjectRiskModelCardsVO cards = new CcdiProjectRiskModelCardsVO();
        cards.setCardList(defaultList(overviewMapper.selectExternalRiskModelCardsByProjectId(projectId)));
        return cards;
    }

    @Override
    public CcdiProjectRiskModelPeopleVO getExternalRiskModelPeople(CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO) {
        ensureProjectExists(queryDTO.getProjectId());
        normalizeExternalRiskModelPeopleQuery(queryDTO);

        Page<CcdiProjectRiskModelPeopleItemVO> page = new Page<>(
            defaultPageNum(queryDTO.getPageNum()),
            defaultPageSize(queryDTO.getPageSize())
        );
        Page<CcdiProjectRiskModelPeopleItemVO> resultPage =
            overviewMapper.selectExternalRiskModelPeoplePage(page, queryDTO);

        List<CcdiProjectRiskModelPeopleItemVO> rows = defaultList(resultPage == null ? null : resultPage.getRecords())
            .stream()
            .peek(item -> item.setActionLabel(ACTION_LABEL))
            .toList();

        CcdiProjectRiskModelPeopleVO people = new CcdiProjectRiskModelPeopleVO();
        people.setRows(rows);
        people.setTotal(resultPage == null ? 0L : resultPage.getTotal());
        return people;
    }

    @Override
    public List<CcdiProjectRiskModelPeopleExcel> exportExternalRiskModelPeople(
        CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO
    ) {
        ensureProjectExists(queryDTO.getProjectId());
        normalizeExternalRiskModelPeopleQuery(queryDTO);

        return defaultList(overviewMapper.selectExternalRiskModelPeopleList(queryDTO)).stream()
            .map(item -> buildRiskModelPeopleExcelRow(item, item.getStaffCode()))
            .toList();
    }

    @Override
    public CcdiProjectSuspiciousTransactionPageVO getSuspiciousTransactions(
        CcdiProjectSuspiciousTransactionQueryDTO queryDTO
    ) {
        ensureProjectExists(queryDTO.getProjectId());
        normalizeSuspiciousTransactionQuery(queryDTO);
        if (isSuspiciousTransactionScopeMismatch(queryDTO)) {
            return emptySuspiciousTransactionPage();
        }
        prepareSuspiciousTransactionExternalBranch(queryDTO);

        if (isExternalScopeWithoutSubject(queryDTO)) {
            return emptySuspiciousTransactionPage();
        }

        Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(
            defaultPageNum(queryDTO.getPageNum()),
            defaultPageSize(queryDTO.getPageSize())
        );
        Page<CcdiProjectSuspiciousTransactionItemVO> resultPage =
            overviewMapper.selectSuspiciousTransactionPage(page, queryDTO);
        List<CcdiProjectSuspiciousTransactionItemVO> rows =
            defaultList(resultPage == null ? null : resultPage.getRecords());
        attachSuspiciousTransactionHitTags(rows, queryDTO);

        CcdiProjectSuspiciousTransactionPageVO result = new CcdiProjectSuspiciousTransactionPageVO();
        result.setRows(rows);
        result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
        return result;
    }

    @Override
    public List<CcdiProjectSuspiciousTransactionExcel> exportSuspiciousTransactions(
        CcdiProjectSuspiciousTransactionQueryDTO queryDTO
    ) {
        ensureProjectExists(queryDTO.getProjectId());
        normalizeSuspiciousTransactionQuery(queryDTO);
        if (isSuspiciousTransactionScopeMismatch(queryDTO)) {
            return List.of();
        }
        prepareSuspiciousTransactionExternalBranch(queryDTO);

        if (isExternalScopeWithoutSubject(queryDTO)) {
            return List.of();
        }

        return defaultList(overviewMapper.selectReportSuspiciousTransactionList(queryDTO)).stream()
            .map(this::buildSuspiciousTransactionExcelRow)
            .toList();
    }

    @Override
    public List<CcdiProjectRiskPeopleOverviewExcel> exportRiskPeopleOverview(Long projectId) {
        ensureProjectExists(projectId);

        return defaultList(overviewMapper.selectRiskPeopleOverviewList(projectId)).stream()
            .map(aggregate -> buildRiskPeopleItem(projectId, aggregate))
            .map(this::buildRiskPeopleExcelRow)
            .toList();
    }

    @Override
    public CcdiProjectEmployeeCreditNegativePageVO getEmployeeCreditNegative(
        CcdiProjectEmployeeCreditNegativeQueryDTO queryDTO
    ) {
        ensureProjectExists(queryDTO.getProjectId());

        Page<CcdiProjectEmployeeCreditNegativeItemVO> page = new Page<>(
            defaultPageNum(queryDTO.getPageNum()),
            defaultPageSize(queryDTO.getPageSize())
        );
        Page<CcdiProjectEmployeeCreditNegativeItemVO> resultPage =
            overviewMapper.selectEmployeeCreditNegativePage(page, queryDTO);

        CcdiProjectEmployeeCreditNegativePageVO result = new CcdiProjectEmployeeCreditNegativePageVO();
        result.setRows(defaultList(resultPage == null ? null : resultPage.getRecords()));
        result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
        return result;
    }

    @Override
    public CcdiProjectAbnormalAccountPageVO getAbnormalAccountPeople(CcdiProjectAbnormalAccountQueryDTO queryDTO) {
        ensureProjectExists(queryDTO.getProjectId());

        Page<CcdiProjectAbnormalAccountItemVO> page = new Page<>(
            defaultAbnormalAccountPageNum(queryDTO.getPageNum()),
            defaultAbnormalAccountPageSize(queryDTO.getPageSize())
        );
        Page<CcdiProjectAbnormalAccountItemVO> resultPage = overviewMapper.selectAbnormalAccountPage(page, queryDTO);

        CcdiProjectAbnormalAccountPageVO result = new CcdiProjectAbnormalAccountPageVO();
        result.setRows(defaultList(resultPage == null ? null : resultPage.getRecords()));
        result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
        return result;
    }

    @Override
    public List<CcdiProjectAbnormalAccountExcel> exportAbnormalAccountPeople(Long projectId) {
        ensureProjectExists(projectId);

        return defaultList(overviewMapper.selectAbnormalAccountList(projectId)).stream()
            .map(this::buildAbnormalAccountExcelRow)
            .toList();
    }

    @Override
    public CcdiProjectUnsubmittedAccountVO getUnsubmittedAccounts(CcdiProjectUnsubmittedAccountQueryDTO queryDTO) {
        ensureProjectExists(queryDTO.getProjectId());

        List<CcdiProjectUnsubmittedAccountItemVO> rows =
            defaultList(overviewMapper.selectUnsubmittedAccountList(queryDTO));
        CcdiProjectUnsubmittedAccountVO result = new CcdiProjectUnsubmittedAccountVO();
        result.setAccountCount(rows.size());
        result.setTotalAmountCr(sumUnsubmittedAmount(rows, true));
        result.setTotalAmountDr(sumUnsubmittedAmount(rows, false));
        result.setNetAmount(result.getTotalAmountCr().subtract(result.getTotalAmountDr()));

        Map<String, List<CcdiProjectUnsubmittedAccountItemVO>> groupedRows = rows.stream()
            .collect(Collectors.groupingBy(
                item -> item.getSubjectName() == null ? "" : item.getSubjectName(),
                LinkedHashMap::new,
                Collectors.toList()
            ));
        result.setSubjects(groupedRows.entrySet().stream()
            .map(entry -> buildUnsubmittedAccountSubject(entry.getKey(), entry.getValue()))
            .toList());
        return result;
    }

    @Override
    public List<CcdiProjectUnsubmittedAccountFlowVO> getUnsubmittedAccountFlows(
        CcdiProjectUnsubmittedAccountQueryDTO queryDTO
    ) {
        ensureProjectExists(queryDTO.getProjectId());

        return defaultList(overviewMapper.selectUnsubmittedAccountFlowList(queryDTO));
    }

    @Override
    public List<CcdiProjectUnsubmittedAccountFlowExcel> exportUnsubmittedAccountFlows(Long projectId) {
        CcdiProjectUnsubmittedAccountQueryDTO queryDTO = new CcdiProjectUnsubmittedAccountQueryDTO();
        queryDTO.setProjectId(projectId);
        return exportUnsubmittedAccountFlows(queryDTO);
    }

    @Override
    public List<CcdiProjectUnsubmittedAccountFlowExcel> exportUnsubmittedAccountFlows(
        CcdiProjectUnsubmittedAccountQueryDTO queryDTO
    ) {
        ensureProjectExists(queryDTO.getProjectId());

        return defaultList(overviewMapper.selectUnsubmittedAccountFlowList(queryDTO)).stream()
            .map(this::buildUnsubmittedAccountFlowExcelRow)
            .toList();
    }

    @Override
    public void exportRiskDetails(HttpServletResponse response, Long projectId) {
        CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
        queryDTO.setProjectId(projectId);
        queryDTO.setSuspiciousType(SUSPICIOUS_TYPE_ALL);
        exportRiskDetails(response, queryDTO);
    }

    @Override
    public void exportRiskDetails(
        HttpServletResponse response,
        CcdiProjectSuspiciousTransactionQueryDTO queryDTO
    ) {
        List<CcdiProjectSuspiciousTransactionExcel> suspiciousRows = exportSuspiciousTransactions(queryDTO);
        Long projectId = queryDTO.getProjectId();
        List<CcdiProjectEmployeeCreditNegativeExcel> creditRows = exportEmployeeCreditNegative(projectId);
        List<CcdiProjectAbnormalAccountExcel> abnormalRows = exportAbnormalAccountPeople(projectId);
        List<CcdiProjectUnsubmittedAccountFlowExcel> unsubmittedRows = exportUnsubmittedAccountFlows(projectId);
        try {
            workbookExporter.export(response, projectId, suspiciousRows, creditRows, abnormalRows, unsubmittedRows);
        } catch (IOException e) {
            throw new ServiceException("导出风险明细失败");
        }
    }

    @Override
    public void exportOverviewReport(HttpServletResponse response, Long projectId) {
        CcdiProject project = getRequiredProject(projectId);

        CcdiProjectSuspiciousTransactionQueryDTO suspiciousQuery = new CcdiProjectSuspiciousTransactionQueryDTO();
        suspiciousQuery.setProjectId(projectId);
        suspiciousQuery.setSuspiciousType("ALL");

        CcdiProjectOverviewReportVO report = new CcdiProjectOverviewReportVO();
        report.setProject(project);
        report.setDashboard(getDashboard(projectId));
        report.setUploadSubjects(defaultList(overviewMapper.selectReportUploadSubjects(projectId)).stream()
            .peek(item -> item.setDataPeriod(formatDataPeriod(item.getMinTrxDate(), item.getMaxTrxDate())))
            .toList());
        report.setParams(buildReportParams(project));
        report.setModelSummaries(defaultList(overviewMapper.selectReportRiskModelSummaries(projectId)));
        report.setExternalRiskSummary(getExternalRiskSummary(projectId));
        report.setExternalModelSummaries(buildExternalReportModelSummaries(projectId));
        report.setRiskPeople(defaultList(overviewMapper.selectReportRiskPeople(projectId)).stream()
            .peek(item -> item.setActionLabel(ACTION_LABEL))
            .toList());
        report.setExternalPersonWarnings(exportExternalPersonWarnings(projectId));
        report.setSuspiciousTransactions(defaultList(
            overviewMapper.selectReportSuspiciousTransactionList(suspiciousQuery)
        ));
        report.setIllegalPeople(exportEmployeeCreditNegative(projectId));
        report.setAbnormalAccounts(exportAbnormalAccountPeople(projectId));
        report.setUnsubmittedAccountFlows(exportUnsubmittedAccountFlows(projectId));

        try {
            reportPdfExporter.export(response, report);
        } catch (IOException e) {
            throw new ServiceException("导出结果总览报告失败");
        }
    }

    @Override
    public List<CcdiProjectEmployeeCreditNegativeExcel> exportEmployeeCreditNegative(Long projectId) {
        ensureProjectExists(projectId);

        return defaultList(overviewMapper.selectEmployeeCreditNegativeList(projectId)).stream()
            .map(this::buildEmployeeCreditNegativeExcelRow)
            .toList();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void refreshOverviewEmployeeResults(Long projectId, String operator) {
        getRequiredProject(projectId);
        overviewEmployeeResultMapper.deleteByProjectId(projectId);
        List<CcdiProjectOverviewEmployeeHitRowVO> hitRows =
            overviewEmployeeResultMapper.selectEmployeeHitRowsByProjectId(projectId);
        List<CcdiProjectOverviewEmployeeResult> results =
            overviewEmployeeResultBuilder.build(projectId, hitRows, operator);

        if (!results.isEmpty()) {
            overviewEmployeeResultMapper.insertBatch(results);
        }

        projectMapper.updateRiskCountsByProjectId(
            projectId,
            countProjectScopeStaff(projectId),
            countRiskLevel(results, "HIGH"),
            countRiskLevel(results, "MEDIUM"),
            countRiskLevel(results, "LOW"),
            operator
        );
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void refreshProjectRiskCounts(Long projectId, String operator) {
        getRequiredProject(projectId);
        Map<String, Object> summary = overviewMapper.selectRiskCountSummaryByProjectId(projectId);
        projectMapper.updateRiskCountsByProjectId(
            projectId,
            countProjectScopeStaff(projectId),
            readCount(summary, "highRiskCount"),
            readCount(summary, "mediumRiskCount"),
            readCount(summary, "lowRiskCount"),
            operator
        );
    }

    private int countProjectScopeStaff(Long projectId) {
        return defaultZero(bankStatementMapper.countMatchedStaffCountByProjectId(projectId));
    }

    private CcdiProjectRiskPeopleOverviewItemVO buildRiskPeopleItem(Long projectId, CcdiProjectEmployeeRiskAggregateVO aggregate) {
        CcdiProjectRiskPeopleOverviewItemVO item = new CcdiProjectRiskPeopleOverviewItemVO();
        item.setName(aggregate.getStaffName());
        item.setIdNo(aggregate.getStaffIdCard());
        item.setDepartment(aggregate.getDeptName());
        item.setRiskCount(defaultZero(aggregate.getHitCount()));
        item.setRiskLevel(resolveRiskLevelName(aggregate.getRiskLevelCode()));
        item.setRiskLevelType(resolveRiskLevelType(aggregate.getRiskLevelCode()));
        item.setModelCount(defaultZero(aggregate.getModelCount()));
        item.setRiskPoint(aggregate.getRiskPoint());
        item.setRiskPointTagList(defaultList(
            overviewMapper.selectRiskHitTagsByScope(projectId, aggregate.getStaffIdCard(), null)
        ));
        item.setActionLabel(ACTION_LABEL);
        return item;
    }

    private CcdiProjectTopRiskPeopleItemVO buildTopRiskPeopleItem(CcdiProjectEmployeeRiskAggregateVO aggregate) {
        CcdiProjectTopRiskPeopleItemVO item = new CcdiProjectTopRiskPeopleItemVO();
        item.setName(aggregate.getStaffName());
        item.setIdNo(aggregate.getStaffIdCard());
        item.setDepartment(aggregate.getDeptName());
        item.setRiskLevel(resolveRiskLevelName(aggregate.getRiskLevelCode()));
        item.setRiskLevelType(resolveRiskLevelType(aggregate.getRiskLevelCode()));
        item.setModelCount(defaultZero(aggregate.getModelCount()));
        item.setActionLabel(ACTION_LABEL);
        return item;
    }

    private CcdiProjectRiskPeopleOverviewExcel buildRiskPeopleExcelRow(CcdiProjectRiskPeopleOverviewItemVO item) {
        CcdiProjectRiskPeopleOverviewExcel row = new CcdiProjectRiskPeopleOverviewExcel();
        row.setName(item.getName());
        row.setIdNo(item.getIdNo());
        row.setDepartment(item.getDepartment());
        row.setRiskCount(item.getRiskCount());
        row.setRiskLevel(item.getRiskLevel());
        row.setModelCount(item.getModelCount());
        row.setRiskPoint(item.getRiskPoint());
        return row;
    }

    private CcdiProjectExternalPersonWarningExcel buildExternalPersonWarningExcelRow(
        CcdiProjectExternalPersonWarningItemVO item
    ) {
        CcdiProjectExternalPersonWarningExcel row = new CcdiProjectExternalPersonWarningExcel();
        row.setName(item.getName());
        row.setIdNo(item.getIdNo());
        row.setSubjectType(item.getSubjectType());
        row.setRiskLevel(item.getRiskLevel());
        row.setModelCount(item.getModelCount());
        row.setRiskPoint(item.getRiskPoint());
        row.setRelatedObject(item.getRelatedObject());
        row.setLatestTradeTime(item.getLatestTradeTime());
        return row;
    }

    private List<CcdiProjectOverviewReportModelSummaryVO> buildExternalReportModelSummaries(Long projectId) {
        return defaultList(overviewMapper.selectExternalRiskModelCardsByProjectId(projectId)).stream()
            .map(this::buildExternalReportModelSummary)
            .toList();
    }

    private CcdiProjectOverviewReportModelSummaryVO buildExternalReportModelSummary(CcdiProjectRiskModelCardVO card) {
        CcdiProjectOverviewReportModelSummaryVO row = new CcdiProjectOverviewReportModelSummaryVO();
        row.setModelCode(card.getModelCode());
        row.setModelName(card.getModelName());
        row.setWarningCount(card.getWarningCount());
        row.setPeopleCount(card.getPeopleCount());
        row.setPeopleNames("-");
        return row;
    }

    private CcdiProjectRiskModelPeopleExcel buildRiskModelPeopleExcelRow(
        CcdiProjectRiskModelPeopleItemVO item,
        String subjectType
    ) {
        CcdiProjectRiskModelPeopleExcel row = new CcdiProjectRiskModelPeopleExcel();
        row.setPersonName(item.getStaffName());
        row.setSubjectType(subjectType);
        row.setIdNo(item.getIdNo());
        row.setScopeName(item.getDepartment());
        row.setModelNames(joinModelNames(item.getModelNames()));
        row.setHitTags(joinHitTagNames(item.getHitTagList()));
        return row;
    }

    private void ensureProjectExists(Long projectId) {
        getRequiredProject(projectId);
    }

    private CcdiProjectRiskExclusion buildRiskExclusion(CcdiProjectRiskExclusionSaveDTO dto) {
        if (dto == null) {
            throw new ServiceException("排除参数不能为空");
        }
        String exclusionType = normalizeExclusionType(dto.getExclusionType());
        String ruleCode = normalizeRequired(dto.getRuleCode(), "规则编码不能为空").toUpperCase();
        String excludeReason = normalizeRequired(dto.getExcludeReason(), "排除原因不能为空");
        if (excludeReason.length() > 1000) {
            throw new ServiceException("排除原因不能超过1000个字符");
        }
        CcdiProjectRiskExclusion exclusion = new CcdiProjectRiskExclusion();
        exclusion.setProjectId(dto.getProjectId());
        exclusion.setRuleCode(ruleCode);
        exclusion.setExclusionType(exclusionType);
        exclusion.setExcludeReason(excludeReason);
        exclusion.setStaffIdCard(normalizeBlankToNull(dto.getStaffIdCard()));
        exclusion.setBankStatementId(dto.getBankStatementId());
        if (EXCLUSION_TYPE_STATEMENT.equals(exclusionType) && exclusion.getBankStatementId() == null) {
            throw new ServiceException("流水型排除必须指定流水ID");
        }
        if (EXCLUSION_TYPE_OBJECT.equals(exclusionType) && (exclusion.getStaffIdCard() == null || exclusion.getStaffIdCard().isBlank())) {
            throw new ServiceException("对象型排除必须指定人员证件号");
        }
        String operator = SecurityUtils.getUsername();
        Date now = new Date();
        exclusion.setCreateBy(operator);
        exclusion.setCreateTime(now);
        exclusion.setUpdateBy(operator);
        exclusion.setUpdateTime(now);
        return exclusion;
    }

    private String normalizeExclusionType(String exclusionType) {
        String type = normalizeRequired(exclusionType, "排除类型不能为空").toUpperCase();
        if (!EXCLUSION_TYPE_STATEMENT.equals(type) && !EXCLUSION_TYPE_OBJECT.equals(type)) {
            throw new ServiceException("排除类型不正确");
        }
        return type;
    }

    private String normalizeRequired(String value, String message) {
        if (value == null || value.isBlank()) {
            throw new ServiceException(message);
        }
        return value.trim();
    }

    private String normalizeBlankToNull(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        return value.trim();
    }

    private void normalizeRiskModelPeopleQuery(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
        if (queryDTO.getMatchMode() == null || queryDTO.getMatchMode().isBlank()) {
            queryDTO.setMatchMode("ANY");
            return;
        }
        queryDTO.setMatchMode(queryDTO.getMatchMode().trim().toUpperCase());
    }

    private void normalizeExternalRiskModelPeopleQuery(CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO) {
        if (queryDTO.getMatchMode() == null || queryDTO.getMatchMode().isBlank()) {
            queryDTO.setMatchMode("ANY");
            return;
        }
        queryDTO.setMatchMode(queryDTO.getMatchMode().trim().toUpperCase());
    }

    private void normalizeSuspiciousTransactionQuery(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
        if (queryDTO.getModelCode() != null && !queryDTO.getModelCode().isBlank()) {
            queryDTO.setModelCode(queryDTO.getModelCode().trim().toUpperCase());
        } else {
            queryDTO.setModelCode(null);
        }
        if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) {
            queryDTO.setSuspiciousType(SUSPICIOUS_TYPE_ALL);
            return;
        }
        queryDTO.setSuspiciousType(queryDTO.getSuspiciousType().trim().toUpperCase());
    }

    private void prepareSuspiciousTransactionExternalBranch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
        if (!shouldCheckExternalPersonBranch(queryDTO)) {
            queryDTO.setIncludeExternalPerson(false);
            return;
        }
        queryDTO.setIncludeExternalPerson(hasExternalPersonSubject(queryDTO.getProjectId()));
    }

    private boolean shouldCheckExternalPersonBranch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
        if (SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType())) {
            return true;
        }
        return SUSPICIOUS_TYPE_ALL.equals(queryDTO.getSuspiciousType())
            && (queryDTO.getModelCode() == null || isExternalModelCode(queryDTO.getModelCode()));
    }

    private boolean hasExternalPersonSubject(Long projectId) {
        return overviewMapper.selectExternalPersonSubjectExistsByProjectId(projectId) != null;
    }

    private boolean isExternalScopeWithoutSubject(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
        return (SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType())
            || isExternalModelCode(queryDTO.getModelCode()))
            && !Boolean.TRUE.equals(queryDTO.getIncludeExternalPerson());
    }

    private boolean isSuspiciousTransactionScopeMismatch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
        if (queryDTO.getModelCode() == null) {
            return false;
        }
        boolean externalModel = isExternalModelCode(queryDTO.getModelCode());
        return (SUSPICIOUS_TYPE_MODEL_RULE.equals(queryDTO.getSuspiciousType()) && externalModel)
            || (SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType()) && !externalModel);
    }

    private boolean isExternalModelCode(String modelCode) {
        return modelCode != null && modelCode.startsWith("EXTERNAL_");
    }

    private CcdiProjectSuspiciousTransactionPageVO emptySuspiciousTransactionPage() {
        CcdiProjectSuspiciousTransactionPageVO result = new CcdiProjectSuspiciousTransactionPageVO();
        result.setRows(List.of());
        result.setTotal(0L);
        return result;
    }

    private CcdiProjectOverviewStatVO buildStat(String key, String label, Integer value) {
        CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
        stat.setKey(key);
        stat.setLabel(label);
        stat.setValue(value);
        return stat;
    }

    private Integer readCount(Map<String, Object> summary, String key) {
        if (summary == null) {
            return 0;
        }
        Object value = summary.get(key);
        if (value == null) {
            return 0;
        }
        if (value instanceof Number number) {
            return number.intValue();
        }
        throw new ServiceException("项目风险人数统计结果类型异常");
    }

    private Integer countRiskLevel(List<CcdiProjectOverviewEmployeeResult> results, String riskLevelCode) {
        return Math.toIntExact(results.stream()
            .filter(item -> riskLevelCode.equals(item.getRiskLevelCode()))
            .count());
    }

    private Integer defaultZero(Integer value) {
        return value == null ? 0 : value;
    }

    private long defaultRiskPeoplePageNum(Integer pageNum) {
        return pageNum == null || pageNum <= 0 ? 1L : pageNum.longValue();
    }

    private long defaultRiskPeoplePageSize(Integer pageSize) {
        return pageSize == null || pageSize <= 0 ? 5L : pageSize.longValue();
    }

    private long defaultAbnormalAccountPageNum(Integer pageNum) {
        return pageNum == null || pageNum <= 0 ? 1L : pageNum.longValue();
    }

    private long defaultAbnormalAccountPageSize(Integer pageSize) {
        return pageSize == null || pageSize <= 0 ? 5L : pageSize.longValue();
    }

    private long defaultPageNum(Integer pageNum) {
        return pageNum == null || pageNum < 1 ? 1L : pageNum.longValue();
    }

    private long defaultPageSize(Integer pageSize) {
        return pageSize == null || pageSize < 1 ? 10L : pageSize.longValue();
    }

    private <T> List<T> defaultList(List<T> value) {
        return value == null ? List.of() : value;
    }

    private CcdiProjectSuspiciousTransactionExcel buildSuspiciousTransactionExcelRow(
        CcdiProjectOverviewReportSuspiciousTransactionVO item
    ) {
        CcdiProjectSuspiciousTransactionExcel row = new CcdiProjectSuspiciousTransactionExcel();
        row.setTrxDate(item.getTrxDate());
        row.setLeAccountNo(item.getLeAccountNo());
        row.setLeAccountName(item.getLeAccountName());
        row.setCustomerAccountName(item.getCustomerAccountName());
        row.setCustomerAccountNo(item.getCustomerAccountNo());
        row.setRelatedStaffDisplay(formatRelatedStaff(item.getRelatedStaffName(), item.getRelatedStaffCode()));
        row.setUserMemo(item.getUserMemo());
        row.setCashType(item.getCashType());
        row.setHitTags(item.getHitTags());
        row.setDisplayAmount(item.getDisplayAmount());
        return row;
    }

    private CcdiProjectEmployeeCreditNegativeExcel buildEmployeeCreditNegativeExcelRow(
        CcdiProjectEmployeeCreditNegativeItemVO item
    ) {
        CcdiProjectEmployeeCreditNegativeExcel row = new CcdiProjectEmployeeCreditNegativeExcel();
        row.setPersonName(item.getPersonName());
        row.setPersonId(item.getPersonId());
        row.setQueryDate(item.getQueryDate());
        row.setCivilCnt(item.getCivilCnt());
        row.setCivilLmt(item.getCivilLmt());
        row.setEnforceCnt(item.getEnforceCnt());
        row.setEnforceLmt(item.getEnforceLmt());
        row.setAdmCnt(item.getAdmCnt());
        row.setAdmLmt(item.getAdmLmt());
        return row;
    }

    private CcdiProjectAbnormalAccountExcel buildAbnormalAccountExcelRow(CcdiProjectAbnormalAccountItemVO item) {
        CcdiProjectAbnormalAccountExcel row = new CcdiProjectAbnormalAccountExcel();
        row.setAccountNo(item.getAccountNo());
        row.setAccountName(item.getAccountName());
        row.setBankName(item.getBankName());
        row.setAbnormalType(item.getAbnormalType());
        row.setAbnormalTime(item.getAbnormalTime());
        row.setStatus(item.getStatus());
        row.setReasonDetail(item.getReasonDetail());
        row.setInvolvedAmount(item.getInvolvedAmount());
        return row;
    }

    private CcdiProjectUnsubmittedAccountSubjectVO buildUnsubmittedAccountSubject(
        String subjectName,
        List<CcdiProjectUnsubmittedAccountItemVO> rows
    ) {
        CcdiProjectUnsubmittedAccountSubjectVO subject = new CcdiProjectUnsubmittedAccountSubjectVO();
        subject.setSubjectName(subjectName);
        subject.setAccountCount(rows.size());
        subject.setTotalAmountCr(sumUnsubmittedAmount(rows, true));
        subject.setTotalAmountDr(sumUnsubmittedAmount(rows, false));
        subject.setNetAmount(subject.getTotalAmountCr().subtract(subject.getTotalAmountDr()));
        subject.setAccounts(rows);
        return subject;
    }

    private BigDecimal sumUnsubmittedAmount(List<CcdiProjectUnsubmittedAccountItemVO> rows, boolean inflow) {
        return rows.stream()
            .map(item -> inflow ? item.getTotalAmountCr() : item.getTotalAmountDr())
            .filter(value -> value != null)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private CcdiProjectUnsubmittedAccountFlowExcel buildUnsubmittedAccountFlowExcelRow(
        CcdiProjectUnsubmittedAccountFlowVO item
    ) {
        CcdiProjectUnsubmittedAccountFlowExcel row = new CcdiProjectUnsubmittedAccountFlowExcel();
        row.setSubjectName(item.getSubjectName());
        row.setAccountNo(item.getAccountNo());
        row.setTrxDate(item.getTrxDate());
        row.setLeAccountNo(item.getLeAccountNo());
        row.setLeAccountName(item.getLeAccountName());
        row.setCustomerAccountName(item.getCustomerAccountName());
        row.setCustomerAccountNo(item.getCustomerAccountNo());
        row.setUserMemo(item.getUserMemo());
        row.setCashType(item.getCashType());
        row.setAmountCr(item.getAmountCr());
        row.setAmountDr(item.getAmountDr());
        return row;
    }

    private List<CcdiProjectOverviewReportParamVO> buildReportParams(CcdiProject project) {
        Long effectiveProjectId = "default".equals(project.getConfigType()) ? 0L : project.getProjectId();
        return defaultList(modelParamMapper.selectByProjectId(effectiveProjectId)).stream()
            .map(this::buildReportParamRow)
            .toList();
    }

    private CcdiProjectOverviewReportParamVO buildReportParamRow(CcdiModelParam param) {
        CcdiProjectOverviewReportParamVO row = new CcdiProjectOverviewReportParamVO();
        row.setModelName(param.getModelName());
        row.setParamName(param.getParamName());
        row.setParamValue(param.getParamValue());
        row.setParamUnit(param.getParamUnit());
        row.setParamDesc(param.getParamDesc());
        return row;
    }

    private String formatDataPeriod(String minTrxDate, String maxTrxDate) {
        if (minTrxDate == null || minTrxDate.isBlank() || maxTrxDate == null || maxTrxDate.isBlank()) {
            return "-";
        }
        LocalDate start = LocalDate.parse(minTrxDate);
        LocalDate end = LocalDate.parse(maxTrxDate);
        int months = (end.getYear() - start.getYear()) * 12 + end.getMonthValue() - start.getMonthValue() + 1;
        return Math.max(months, 1) + "个月";
    }

    private String formatRelatedStaff(String relatedStaffName, String relatedStaffCode) {
        if (relatedStaffName == null || relatedStaffName.isBlank()) {
            return null;
        }
        if (relatedStaffCode == null || relatedStaffCode.isBlank()) {
            return relatedStaffName;
        }
        return relatedStaffName + "(" + relatedStaffCode + ")";
    }

    private String formatSummaryAndCashType(String userMemo, String cashType) {
        String safeMemo = userMemo == null ? "" : userMemo;
        String safeCashType = cashType == null ? "" : cashType;
        return safeMemo + "/" + safeCashType;
    }

    private String joinModelNames(List<String> modelNames) {
        return defaultList(modelNames).stream()
            .filter(item -> item != null && !item.isBlank())
            .distinct()
            .collect(Collectors.joining("、"));
    }

    private String joinHitTagNames(List<CcdiProjectRiskHitTagVO> hitTags) {
        return defaultList(hitTags).stream()
            .map(CcdiProjectRiskHitTagVO::getRuleName)
            .filter(item -> item != null && !item.isBlank())
            .distinct()
            .collect(Collectors.joining("、"));
    }

    private CcdiProjectPersonAnalysisAbnormalDetailVO buildAbnormalDetail(
        List<CcdiBankStatementListVO> statementRows,
        List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows
    ) {
        List<CcdiProjectPersonAnalysisAbnormalGroupVO> groups = new ArrayList<>();
        if (!statementRows.isEmpty()) {
            groups.add(buildAbnormalGroup("BANK_STATEMENT", "流水异常明细", "BANK_STATEMENT", statementRows));
        }
        if (!objectRows.isEmpty()) {
            groups.add(buildAbnormalGroup("RELATED_OBJECT", "异常对象摘要", "OBJECT", objectRows));
        }
        CcdiProjectPersonAnalysisAbnormalDetailVO abnormalDetail = new CcdiProjectPersonAnalysisAbnormalDetailVO();
        abnormalDetail.setGroups(groups);
        return abnormalDetail;
    }

    private CcdiProjectPersonAnalysisAbnormalGroupVO buildAbnormalGroup(
        String groupCode,
        String groupName,
        String groupType,
        List<?> records
    ) {
        CcdiProjectPersonAnalysisAbnormalGroupVO group = new CcdiProjectPersonAnalysisAbnormalGroupVO();
        group.setGroupCode(groupCode);
        group.setGroupName(groupName);
        group.setGroupType(groupType);
        group.setRecords(records);
        return group;
    }

    private void attachStatementHitTags(List<CcdiBankStatementListVO> statementRows, Long projectId) {
        if (statementRows.isEmpty() || projectId == null) {
            return;
        }
        List<Long> bankStatementIds = statementRows.stream()
            .map(CcdiBankStatementListVO::getBankStatementId)
            .filter(item -> item != null)
            .distinct()
            .collect(Collectors.toList());
        if (bankStatementIds.isEmpty()) {
            return;
        }
        Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = defaultList(
            bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
                projectId,
                bankStatementIds,
                null,
                null
            )
        ).stream().filter(item -> item.getBankStatementId() != null)
            .collect(Collectors.groupingBy(
                CcdiBankStatementHitTagVO::getBankStatementId,
                LinkedHashMap::new,
                Collectors.toList()
            ));
        statementRows.forEach(row -> row.setHitTags(new ArrayList<>(
            hitTagMap.getOrDefault(row.getBankStatementId(), Collections.emptyList())
        )));
    }

    private void attachSuspiciousTransactionHitTags(
        List<CcdiProjectSuspiciousTransactionItemVO> rows,
        CcdiProjectSuspiciousTransactionQueryDTO queryDTO
    ) {
        if (rows.isEmpty()) {
            return;
        }
        List<Long> bankStatementIds = rows.stream()
            .map(CcdiProjectSuspiciousTransactionItemVO::getBankStatementId)
            .filter(item -> item != null)
            .distinct()
            .toList();
        if (bankStatementIds.isEmpty()) {
            return;
        }
        Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = defaultList(
            bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
                queryDTO.getProjectId(),
                bankStatementIds,
                queryDTO.getModelCode(),
                queryDTO.getSuspiciousType()
            )
        ).stream().filter(item -> item.getBankStatementId() != null)
            .collect(Collectors.groupingBy(
                CcdiBankStatementHitTagVO::getBankStatementId,
                LinkedHashMap::new,
                Collectors.toList()
            ));
        rows.forEach(row -> row.setHitTags(new ArrayList<>(
            hitTagMap.getOrDefault(row.getBankStatementId(), Collections.emptyList())
        )));
    }

    private void normalizeObjectRows(List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows) {
        objectRows.forEach(row -> {
            if (row.getRiskTags() == null) {
                row.setRiskTags(new ArrayList<>());
            }
            if (row.getExtraFields() == null) {
                row.setExtraFields(new ArrayList<>());
            }
        });
    }

    private CcdiProject getRequiredProject(Long projectId) {
        CcdiProject project = projectMapper.selectById(projectId);
        if (project == null) {
            throw new ServiceException("项目不存在");
        }
        return project;
    }

    private String resolveRiskLevelName(String riskLevelCode) {
        return switch (riskLevelCode) {
            case "HIGH" -> "高风险";
            case "MEDIUM" -> "中风险";
            default -> "低风险";
        };
    }

    private String resolveRiskLevelType(String riskLevelCode) {
        return switch (riskLevelCode) {
            case "HIGH" -> "danger";
            case "MEDIUM" -> "warning";
            default -> "info";
        };
    }
}
