package com.gxwebsoft.credit.controller;

import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.credit.entity.CreditJudgmentDebtor;
import com.gxwebsoft.credit.param.CreditJudgmentDebtorImportParam;
import com.gxwebsoft.credit.param.CreditJudgmentDebtorParam;
import com.gxwebsoft.credit.service.CreditCompanyService;
import com.gxwebsoft.credit.service.CreditCompanyRecordCountService;
import com.gxwebsoft.credit.service.CreditJudgmentDebtorService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Locale;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * 被执行人控制器
 *
 * @author 科技小王子
 * @since 2025-12-19 19:50:55
 */
@Tag(name = "被执行人管理")
@RestController
@RequestMapping("/api/credit/credit-judgment-debtor")
public class CreditJudgmentDebtorController extends BaseController {
    @Resource
    private CreditJudgmentDebtorService creditJudgmentDebtorService;

    @Resource
    private BatchImportSupport batchImportSupport;

    @Resource
    private CreditCompanyService creditCompanyService;

    @Resource
    private CreditCompanyRecordCountService creditCompanyRecordCountService;

    @Operation(summary = "分页查询被执行人")
    @GetMapping("/page")
    public ApiResult<PageResult<CreditJudgmentDebtor>> page(CreditJudgmentDebtorParam param) {
        // 使用关联查询
        return success(creditJudgmentDebtorService.pageRel(param));
    }

    @Operation(summary = "查询全部被执行人")
    @GetMapping()
    public ApiResult<List<CreditJudgmentDebtor>> list(CreditJudgmentDebtorParam param) {
        // 使用关联查询
        return success(creditJudgmentDebtorService.listRel(param));
    }

    @Operation(summary = "根据id查询被执行人")
    @GetMapping("/{id}")
    public ApiResult<CreditJudgmentDebtor> get(@PathVariable("id") Integer id) {
        // 使用关联查询
        return success(creditJudgmentDebtorService.getByIdRel(id));
    }

    @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:save')")
    @OperationLog
    @Operation(summary = "添加被执行人")
    @PostMapping()
    public ApiResult<?> save(@RequestBody CreditJudgmentDebtor creditJudgmentDebtor) {
        // 记录当前登录用户id
        // User loginUser = getLoginUser();
        // if (loginUser != null) {
        // creditJudgmentDebtor.setUserId(loginUser.getUserId());
        // }
        if (creditJudgmentDebtorService.save(creditJudgmentDebtor)) {
            return success("添加成功");
        }
        return fail("添加失败");
    }

    @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:update')")
    @OperationLog
    @Operation(summary = "修改被执行人")
    @PutMapping()
    public ApiResult<?> update(@RequestBody CreditJudgmentDebtor creditJudgmentDebtor) {
        if (creditJudgmentDebtorService.updateById(creditJudgmentDebtor)) {
            return success("修改成功");
        }
        return fail("修改失败");
    }

    @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:remove')")
    @OperationLog
    @Operation(summary = "删除被执行人")
    @DeleteMapping("/{id}")
    public ApiResult<?> remove(@PathVariable("id") Integer id) {
        if (creditJudgmentDebtorService.removeById(id)) {
            return success("删除成功");
        }
        return fail("删除失败");
    }

    @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:save')")
    @OperationLog
    @Operation(summary = "批量添加被执行人")
    @PostMapping("/batch")
    public ApiResult<?> saveBatch(@RequestBody List<CreditJudgmentDebtor> list) {
        if (creditJudgmentDebtorService.saveBatch(list)) {
            return success("添加成功");
        }
        return fail("添加失败");
    }

    @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:update')")
    @OperationLog
    @Operation(summary = "批量修改被执行人")
    @PutMapping("/batch")
    public ApiResult<?> removeBatch(@RequestBody BatchParam<CreditJudgmentDebtor> batchParam) {
        if (batchParam.update(creditJudgmentDebtorService, "id")) {
            return success("修改成功");
        }
        return fail("修改失败");
    }

    @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:remove')")
    @OperationLog
    @Operation(summary = "批量删除被执行人")
    @DeleteMapping("/batch")
    public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
        if (creditJudgmentDebtorService.removeByIds(ids)) {
            return success("删除成功");
        }
        return fail("删除失败");
    }

    /**
     * 根据企业名称匹配企业并更新 companyId（匹配 CreditCompany.name / CreditCompany.matchName）
     *
     * <p>默认仅更新 companyId=0 的记录；如需覆盖更新，传 onlyNull=false。</p>
     */
    @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:update')")
    @OperationLog
    @Operation(summary = "根据企业名称匹配并更新companyId")
    @PostMapping("/company-id/refresh")
    public ApiResult<Map<String, Object>> refreshCompanyIdByCompanyName(
        @RequestParam(value = "onlyNull", required = false, defaultValue = "true") Boolean onlyNull,
        @RequestParam(value = "limit", required = false) Integer limit
    ) {
        User loginUser = getLoginUser();
        Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;

        BatchImportSupport.CompanyIdRefreshStats stats = batchImportSupport.refreshCompanyIdByCompanyName(
            creditJudgmentDebtorService,
            creditCompanyService,
            currentTenantId,
            onlyNull,
            limit,
            CreditJudgmentDebtor::getId,
            CreditJudgmentDebtor::setId,
            CreditJudgmentDebtor::getName,
            CreditJudgmentDebtor::getCompanyId,
            CreditJudgmentDebtor::setCompanyId,
            CreditJudgmentDebtor::getHasData,
            CreditJudgmentDebtor::setHasData,
            CreditJudgmentDebtor::getTenantId,
            CreditJudgmentDebtor::new
        );

        if (!stats.anyDataRead) {
            return success("无可更新数据", stats.toMap());
        }
        return success("更新完成，更新" + stats.updated + "条", stats.toMap());
    }

    /**
     * 批量导入被执行人
     */
    @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:save')")
    @Operation(summary = "批量导入被执行人")
    @PostMapping("/import")
    public ApiResult<List<String>> importBatch(@RequestParam("file") MultipartFile file,
                                               @RequestParam(value = "companyId", required = false) Integer companyId) {
        try {
            User loginUser = getLoginUser();
            Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
            Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;

            ImportOutcome outcome;
            if (isZip(file)) {
                outcome = importFromZip(file, currentUserId, currentTenantId, companyId);
            } else {
                outcome = importFromExcel(file, safeFileLabel(file.getOriginalFilename()), currentUserId, currentTenantId, companyId, false);
            }

            if (!outcome.anyDataRead) {
                return fail("未读取到数据，请确认模板表头与示例格式一致", null);
            }

            creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.JUDGMENT_DEBTOR, outcome.touchedCompanyIds);

            if (outcome.errorMessages.isEmpty()) {
                return success("成功导入" + outcome.successCount + "条数据", null);
            }
            return success("导入完成，成功" + outcome.successCount + "条，失败" + outcome.errorMessages.size() + "条", outcome.errorMessages);
        } catch (Exception e) {
            e.printStackTrace();
            return fail("导入失败：" + e.getMessage(), null);
        }
    }

    /**
     * 批量导入历史被执行人（写入被执行人表 credit_judgment_debtor，仅解析“历史被执行人”选项卡）
     * 规则：案号相同则更新；案号不存在则插入；导入文件内案号重复时取最后一条覆盖。
     */
    @PreAuthorize("hasAuthority('credit:creditJudgmentDebtor:save')")
    @Operation(summary = "批量导入历史被执行人")
    @PostMapping("/import/history")
    public ApiResult<List<String>> importHistoryBatch(@RequestParam("file") MultipartFile file,
                                                      @RequestParam(value = "companyId", required = false) Integer companyId) {
        try {
            User loginUser = getLoginUser();
            Integer currentUserId = loginUser != null ? loginUser.getUserId() : null;
            Integer currentTenantId = loginUser != null ? loginUser.getTenantId() : null;

            ImportOutcome outcome;
            if (isZip(file)) {
                outcome = importHistoryFromZip(file, currentUserId, currentTenantId, companyId);
            } else {
                outcome = importHistoryFromExcel(file, safeFileLabel(file.getOriginalFilename()), currentUserId, currentTenantId, companyId);
            }

            if (!outcome.anyDataRead) {
                return fail("未读取到数据，请确认文件中存在“历史被执行人”选项卡且表头与示例格式一致", null);
            }

            creditCompanyRecordCountService.refresh(CreditCompanyRecordCountService.CountType.JUDGMENT_DEBTOR, outcome.touchedCompanyIds);

            if (outcome.errorMessages.isEmpty()) {
                return success("成功导入" + outcome.successCount + "条数据", null);
            }
            return success("导入完成，成功" + outcome.successCount + "条，失败" + outcome.errorMessages.size() + "条", outcome.errorMessages);
        } catch (Exception e) {
            e.printStackTrace();
            return fail("导入失败：" + e.getMessage(), null);
        }
    }

    /**
     * 下载被执行人导入模板
     */
    @Operation(summary = "下载被执行人导入模板")
    @GetMapping("/import/template")
    public void downloadTemplate(HttpServletResponse response) throws IOException {
        List<CreditJudgmentDebtorImportParam> templateList = new ArrayList<>();

        CreditJudgmentDebtorImportParam example = new CreditJudgmentDebtorImportParam();
        example.setCaseNumber("（2024）示例案号");
        example.setName("某某公司");
        example.setCode("1234567890");
        example.setOccurrenceTime("2024-01-10");
        example.setAmount("100000");
        example.setDataStatus("已公开");
        example.setComments("备注信息");
        templateList.add(example);

        Workbook workbook = ExcelImportSupport.buildTemplate("被执行人导入模板", "被执行人", CreditJudgmentDebtorImportParam.class, templateList);

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", "attachment; filename=credit_judgment_debtor_import_template.xlsx");

        workbook.write(response.getOutputStream());
        workbook.close();
    }

    private boolean isEmptyImportRow(CreditJudgmentDebtorImportParam param) {
        if (param == null) {
            return true;
        }
        if (isImportHeaderRow(param)) {
            return true;
        }
        return ImportHelper.isBlank(param.getCaseNumber());
    }

    private boolean isImportHeaderRow(CreditJudgmentDebtorImportParam param) {
        return isHeaderValue(param.getName(), "序号")
            || isHeaderValue(param.getName1(), "序号")
            || isHeaderValue(param.getCaseNumber(), "案号")
            || isHeaderValue(param.getName(), "被执行人名称")
            || isHeaderValue(param.getName1(), "被执行人")
            || isHeaderValue(param.getCode(), "证件号/组织机构代码")
            || isHeaderValue(param.getOccurrenceTime(), "立案日期")
            || isHeaderValue(param.getCourtName(), "法院")
            || isHeaderValue(param.getAmount(), "执行标的(元)")
            || isHeaderValue(param.getDataStatus(), "数据状态");
    }

    private static boolean isHeaderValue(String value, String headerText) {
        if (value == null) {
            return false;
        }
        return headerText.equals(value.trim());
    }

    private CreditJudgmentDebtor convertImportParamToEntity(CreditJudgmentDebtorImportParam param) {
        CreditJudgmentDebtor entity = new CreditJudgmentDebtor();

        entity.setCaseNumber(param.getCaseNumber());
        entity.setName1(param.getName1());
        String debtorName = ImportHelper.isBlank(param.getName()) ? param.getName1() : param.getName();
        entity.setName(debtorName);
        entity.setCode(param.getCode());
        entity.setOccurrenceTime(param.getOccurrenceTime());
        entity.setAmount(param.getAmount());
        entity.setCourtName(param.getCourtName());
        entity.setDataStatus(param.getDataStatus());
        entity.setComments(param.getComments());

        return entity;
    }

    private static class ImportOutcome {
        private final boolean anyDataRead;
        private final int successCount;
        private final List<String> errorMessages;
        private final Set<Integer> touchedCompanyIds;

        private ImportOutcome(boolean anyDataRead, int successCount, List<String> errorMessages, Set<Integer> touchedCompanyIds) {
            this.anyDataRead = anyDataRead;
            this.successCount = successCount;
            this.errorMessages = errorMessages;
            this.touchedCompanyIds = touchedCompanyIds != null ? touchedCompanyIds : new HashSet<>();
        }
    }

    private ImportOutcome importFromExcel(MultipartFile excelFile, String fileLabel, Integer currentUserId, Integer currentTenantId, Integer companyId, boolean strictDebtorSheet) throws Exception {
        List<String> errorMessages = new ArrayList<>();
        int successCount = 0;
        Set<Integer> touchedCompanyIds = new HashSet<>();

        ExcelImportSupport.ImportResult<CreditJudgmentDebtorImportParam> importResult = readDebtorImport(excelFile, strictDebtorSheet);
        List<CreditJudgmentDebtorImportParam> list = importResult.getData();
        int usedTitleRows = importResult.getTitleRows();
        int usedHeadRows = importResult.getHeadRows();
        int usedSheetIndex = importResult.getSheetIndex();

        if (CollectionUtils.isEmpty(list)) {
            return new ImportOutcome(false, 0, errorMessages, touchedCompanyIds);
        }

        Map<String, String> urlByCaseNumber = ExcelImportSupport.readHyperlinksByHeaderKey(excelFile, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
        Map<String, String> urlByName = ExcelImportSupport.readHyperlinksByHeaderKey(excelFile, usedSheetIndex, usedTitleRows, usedHeadRows, "被执行人名称");
        Map<String, String> urlByName1 = ExcelImportSupport.readHyperlinksByHeaderKey(excelFile, usedSheetIndex, usedTitleRows, usedHeadRows, "被执行人");

        String prefix = ImportHelper.isBlank(fileLabel) ? "" : "【" + fileLabel + "】";
        final int chunkSize = 500;
        final int mpBatchSize = 500;
        List<CreditJudgmentDebtor> chunkItems = new ArrayList<>(chunkSize);
        List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);
        for (int i = 0; i < list.size(); i++) {
            CreditJudgmentDebtorImportParam param = list.get(i);
            try {
                CreditJudgmentDebtor item = convertImportParamToEntity(param);
                String link = null;
                if (!ImportHelper.isBlank(item.getCaseNumber())) {
                    link = urlByCaseNumber.get(item.getCaseNumber().trim());
                }
                if ((link == null || link.isEmpty()) && !ImportHelper.isBlank(item.getName())) {
                    link = urlByName.get(item.getName().trim());
                }
                if ((link == null || link.isEmpty()) && !ImportHelper.isBlank(item.getName1())) {
                    link = urlByName1.get(item.getName1().trim());
                }
                if (link != null && !link.isEmpty()) {
                    item.setUrl(link);
                }

                if (item.getCompanyId() == null && companyId != null) {
                    item.setCompanyId(companyId);
                }
                if (item.getCompanyId() != null && item.getCompanyId() > 0) {
                    touchedCompanyIds.add(item.getCompanyId());
                }
                if (item.getUserId() == null && currentUserId != null) {
                    item.setUserId(currentUserId);
                }
                if (item.getTenantId() == null && currentTenantId != null) {
                    item.setTenantId(currentTenantId);
                }
                if (item.getStatus() == null) {
                    item.setStatus(0);
                }
                if (item.getRecommend() == null) {
                    item.setRecommend(0);
                }
                if (item.getDeleted() == null) {
                    item.setDeleted(0);
                }
                int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
                if (ImportHelper.isBlank(item.getCaseNumber())) {
                    errorMessages.add(prefix + "第" + excelRowNumber + "行：案号不能为空");
                    continue;
                }

                if (item.getCompanyId() != null && item.getCompanyId() > 0) {
                    touchedCompanyIds.add(item.getCompanyId());
                }

                chunkItems.add(item);
                chunkRowNumbers.add(excelRowNumber);
                if (chunkItems.size() >= chunkSize) {
                    successCount += persistImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages);
                    chunkItems.clear();
                    chunkRowNumbers.clear();
                }
            } catch (Exception e) {
                int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
                errorMessages.add(prefix + "第" + excelRowNumber + "行：" + e.getMessage());
                e.printStackTrace();
            }
        }
        if (!chunkItems.isEmpty()) {
            successCount += persistImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages);
        }
        return new ImportOutcome(true, successCount, errorMessages, touchedCompanyIds);
    }

    private ImportOutcome importHistoryFromExcel(MultipartFile excelFile, String fileLabel, Integer currentUserId, Integer currentTenantId, Integer companyId) throws Exception {
        List<String> errorMessages = new ArrayList<>();
        int successCount = 0;
        Set<Integer> touchedCompanyIds = new HashSet<>();

        int historySheetIndex = ExcelImportSupport.findSheetIndex(excelFile, "历史被执行人");
        if (historySheetIndex < 0) {
            return new ImportOutcome(false, 0, errorMessages, touchedCompanyIds);
        }

        ExcelImportSupport.ImportResult<CreditJudgmentDebtorImportParam> importResult = ExcelImportSupport.readBest(
            excelFile,
            CreditJudgmentDebtorImportParam.class,
            this::isEmptyImportRow,
            this::isScoreImportRow,
            historySheetIndex
        );

        List<CreditJudgmentDebtorImportParam> list = importResult.getData();
        int usedTitleRows = importResult.getTitleRows();
        int usedHeadRows = importResult.getHeadRows();
        int usedSheetIndex = importResult.getSheetIndex();

        if (CollectionUtils.isEmpty(list)) {
            return new ImportOutcome(false, 0, errorMessages, touchedCompanyIds);
        }

        Map<String, String> urlByCaseNumber = ExcelImportSupport.readHyperlinksByHeaderKey(excelFile, usedSheetIndex, usedTitleRows, usedHeadRows, "案号");
        Map<String, String> urlByName = ExcelImportSupport.readHyperlinksByHeaderKey(excelFile, usedSheetIndex, usedTitleRows, usedHeadRows, "被执行人名称");
        Map<String, String> urlByName1 = ExcelImportSupport.readHyperlinksByHeaderKey(excelFile, usedSheetIndex, usedTitleRows, usedHeadRows, "被执行人");

        String prefix = ImportHelper.isBlank(fileLabel) ? "" : "【" + fileLabel + "】";

        // 同案号多条：以导入文件中“最后一条”为准（视为最新），避免批处理中重复 upsert。
        LinkedHashMap<String, CreditJudgmentDebtor> latestByCaseNumber = new LinkedHashMap<>();
        LinkedHashMap<String, Integer> latestRowByCaseNumber = new LinkedHashMap<>();

        for (int i = 0; i < list.size(); i++) {
            CreditJudgmentDebtorImportParam param = list.get(i);
            int excelRowNumber = i + 1 + usedTitleRows + usedHeadRows;
            try {
                CreditJudgmentDebtor item = convertImportParamToEntity(param);

                if (item.getCaseNumber() != null) {
                    item.setCaseNumber(item.getCaseNumber().trim());
                }
                if (ImportHelper.isBlank(item.getCaseNumber())) {
                    errorMessages.add(prefix + "第" + excelRowNumber + "行：案号不能为空");
                    continue;
                }

                String link = urlByCaseNumber.get(item.getCaseNumber());
                if ((link == null || link.isEmpty()) && !ImportHelper.isBlank(item.getName())) {
                    link = urlByName.get(item.getName().trim());
                }
                if ((link == null || link.isEmpty()) && !ImportHelper.isBlank(item.getName1())) {
                    link = urlByName1.get(item.getName1().trim());
                }
                if (link != null && !link.isEmpty()) {
                    item.setUrl(link);
                }

                if (item.getCompanyId() == null && companyId != null) {
                    item.setCompanyId(companyId);
                }
                if (item.getUserId() == null && currentUserId != null) {
                    item.setUserId(currentUserId);
                }
                if (item.getTenantId() == null && currentTenantId != null) {
                    item.setTenantId(currentTenantId);
                }
                if (item.getStatus() == null) {
                    item.setStatus(0);
                }
                if (item.getRecommend() == null) {
                    item.setRecommend(0);
                }
                if (item.getDeleted() == null) {
                    item.setDeleted(0);
                }

                latestByCaseNumber.put(item.getCaseNumber(), item);
                latestRowByCaseNumber.put(item.getCaseNumber(), excelRowNumber);
            } catch (Exception e) {
                errorMessages.add(prefix + "第" + excelRowNumber + "行：" + e.getMessage());
                e.printStackTrace();
            }
        }

        if (latestByCaseNumber.isEmpty()) {
            return new ImportOutcome(true, 0, errorMessages, touchedCompanyIds);
        }

        final int chunkSize = 500;
        final int mpBatchSize = 500;
        List<CreditJudgmentDebtor> chunkItems = new ArrayList<>(chunkSize);
        List<Integer> chunkRowNumbers = new ArrayList<>(chunkSize);

        for (Map.Entry<String, CreditJudgmentDebtor> entry : latestByCaseNumber.entrySet()) {
            String caseNumber = entry.getKey();
            CreditJudgmentDebtor item = entry.getValue();
            Integer rowNo = latestRowByCaseNumber.get(caseNumber);
            chunkItems.add(item);
            chunkRowNumbers.add(rowNo != null ? rowNo : -1);
            if (chunkItems.size() >= chunkSize) {
                successCount += persistHistoryImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages);
                chunkItems.clear();
                chunkRowNumbers.clear();
            }
        }
        if (!chunkItems.isEmpty()) {
            successCount += persistHistoryImportChunk(chunkItems, chunkRowNumbers, prefix, mpBatchSize, errorMessages);
        }

        return new ImportOutcome(true, successCount, errorMessages, touchedCompanyIds);
    }

    private int persistHistoryImportChunk(List<CreditJudgmentDebtor> items,
                                          List<Integer> excelRowNumbers,
                                          String prefix,
                                          int mpBatchSize,
                                          List<String> errorMessages) {
        if (CollectionUtils.isEmpty(items)) {
            return 0;
        }

        try {
            return batchImportSupport.runInNewTx(() -> {
                List<String> keys = new ArrayList<>(items.size());
                for (CreditJudgmentDebtor item : items) {
                    if (item == null || ImportHelper.isBlank(item.getCaseNumber())) {
                        continue;
                    }
                    keys.add(item.getCaseNumber().trim());
                }

                Map<String, CreditJudgmentDebtor> existingByCaseNumber = new java.util.HashMap<>();
                if (!keys.isEmpty()) {
                    List<CreditJudgmentDebtor> existingList = creditJudgmentDebtorService.lambdaQuery()
                        .in(CreditJudgmentDebtor::getCaseNumber, keys)
                        .select(CreditJudgmentDebtor::getId, CreditJudgmentDebtor::getCaseNumber, CreditJudgmentDebtor::getRecommend)
                        .list();
                    for (CreditJudgmentDebtor existing : existingList) {
                        if (existing == null || ImportHelper.isBlank(existing.getCaseNumber())) {
                            continue;
                        }
                        existingByCaseNumber.putIfAbsent(existing.getCaseNumber().trim(), existing);
                    }
                }

                List<CreditJudgmentDebtor> updates = new ArrayList<>();
                List<CreditJudgmentDebtor> inserts = new ArrayList<>();
                for (CreditJudgmentDebtor item : items) {
                    if (item == null || ImportHelper.isBlank(item.getCaseNumber())) {
                        continue;
                    }
                    String caseNumber = item.getCaseNumber().trim();
                    CreditJudgmentDebtor existing = existingByCaseNumber.get(caseNumber);
                    if (existing != null && existing.getId() != null) {
                        // 覆盖更新：recommend 记录“被更新次数”，每次更新 +1
                        item.setId(existing.getId());
                        Integer old = existing.getRecommend();
                        item.setRecommend(old == null ? 1 : old + 1);
                        updates.add(item);
                    } else {
                        if (item.getRecommend() == null) {
                            item.setRecommend(0);
                        }
                        inserts.add(item);
                    }
                }

                if (!updates.isEmpty()) {
                    creditJudgmentDebtorService.updateBatchById(updates, mpBatchSize);
                }
                if (!inserts.isEmpty()) {
                    creditJudgmentDebtorService.saveBatch(inserts, mpBatchSize);
                }
                return updates.size() + inserts.size();
            });
        } catch (Exception batchException) {
            int successCount = 0;
            for (int i = 0; i < items.size(); i++) {
                CreditJudgmentDebtor item = items.get(i);
                int excelRowNumber = (excelRowNumbers != null && i < excelRowNumbers.size()) ? excelRowNumbers.get(i) : -1;
                try {
                    int delta = batchImportSupport.runInNewTx(() -> {
                        if (item == null || ImportHelper.isBlank(item.getCaseNumber())) {
                            return 0;
                        }
                        String caseNumber = item.getCaseNumber().trim();
                        CreditJudgmentDebtor existing = creditJudgmentDebtorService.lambdaQuery()
                            .eq(CreditJudgmentDebtor::getCaseNumber, caseNumber)
                            .select(CreditJudgmentDebtor::getId, CreditJudgmentDebtor::getRecommend)
                            .one();
                        if (existing != null && existing.getId() != null) {
                            item.setId(existing.getId());
                            Integer old = existing.getRecommend();
                            item.setRecommend(old == null ? 1 : old + 1);
                            return creditJudgmentDebtorService.updateById(item) ? 1 : 0;
                        }
                        if (item.getRecommend() == null) {
                            item.setRecommend(0);
                        }
                        return creditJudgmentDebtorService.save(item) ? 1 : 0;
                    });
                    if (delta > 0) {
                        successCount += delta;
                    } else {
                        errorMessages.add(prefix + "第" + excelRowNumber + "行：保存失败");
                    }
                } catch (Exception e) {
                    errorMessages.add(prefix + "第" + excelRowNumber + "行：" + e.getMessage());
                }
            }
            return successCount;
        }
    }

    private int persistImportChunk(List<CreditJudgmentDebtor> items,
                                   List<Integer> excelRowNumbers,
                                   String prefix,
                                   int mpBatchSize,
                                   List<String> errorMessages) {
        if (CollectionUtils.isEmpty(items)) {
            return 0;
        }
        try {
            return batchImportSupport.runInNewTx(() -> batchImportSupport.upsertBySingleKey(
                creditJudgmentDebtorService,
                items,
                CreditJudgmentDebtor::getId,
                CreditJudgmentDebtor::setId,
                CreditJudgmentDebtor::getCaseNumber,
                CreditJudgmentDebtor::getCaseNumber,
                null,
                mpBatchSize
            ));
        } catch (Exception batchException) {
            int successCount = 0;
            for (int i = 0; i < items.size(); i++) {
                CreditJudgmentDebtor item = items.get(i);
                int excelRowNumber = (excelRowNumbers != null && i < excelRowNumbers.size()) ? excelRowNumbers.get(i) : -1;
                try {
                    int delta = batchImportSupport.runInNewTx(() -> {
                        boolean saved = creditJudgmentDebtorService.save(item);
                        if (!saved) {
                            CreditJudgmentDebtor existing = creditJudgmentDebtorService.lambdaQuery()
                                .eq(CreditJudgmentDebtor::getCaseNumber, item.getCaseNumber())
                                .one();
                            if (existing != null) {
                                item.setId(existing.getId());
                                if (creditJudgmentDebtorService.updateById(item)) {
                                    return 1;
                                }
                            }
                        } else {
                            return 1;
                        }
                        return 0;
                    });
                    if (delta > 0) {
                        successCount += delta;
                    } else {
                        errorMessages.add(prefix + "第" + excelRowNumber + "行：保存失败");
                    }
                } catch (Exception e) {
                    errorMessages.add(prefix + "第" + excelRowNumber + "行：" + e.getMessage());
                }
            }
            return successCount;
        }
    }

    private ImportOutcome importFromZip(MultipartFile zipFile, Integer currentUserId, Integer currentTenantId, Integer companyId) throws Exception {
        try {
            return importFromZip(zipFile, currentUserId, currentTenantId, companyId, StandardCharsets.UTF_8);
        } catch (IllegalArgumentException e) {
            return importFromZip(zipFile, currentUserId, currentTenantId, companyId, Charset.forName("GBK"));
        }
    }

    private ImportOutcome importFromZip(MultipartFile zipFile, Integer currentUserId, Integer currentTenantId, Integer companyId, Charset charset) throws Exception {
        List<String> errorMessages = new ArrayList<>();
        int successCount = 0;
        boolean anyDataRead = false;
        Set<Integer> touchedCompanyIds = new HashSet<>();

        try (InputStream is = zipFile.getInputStream(); ZipInputStream zis = new ZipInputStream(is, charset)) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                if (entry.isDirectory()) {
                    continue;
                }
                String entryName = entry.getName();
                if (!isExcelFileName(entryName)) {
                    continue;
                }

                byte[] bytes = readAllBytes(zis);
                String entryFileName = safeFileLabel(entryName);
                MultipartFile excelFile = new InMemoryMultipartFile(entryFileName, bytes);

                try {
                    ImportOutcome outcome = importFromExcel(excelFile, entryFileName, currentUserId, currentTenantId, companyId, true);
                    if (outcome.anyDataRead) {
                        anyDataRead = true;
                        successCount += outcome.successCount;
                        errorMessages.addAll(outcome.errorMessages);
                        touchedCompanyIds.addAll(outcome.touchedCompanyIds);
                    }
                } catch (Exception e) {
                    errorMessages.add("【" + entryFileName + "】解析失败：" + e.getMessage());
                }
            }
        }
        return new ImportOutcome(anyDataRead, successCount, errorMessages, touchedCompanyIds);
    }

    private ImportOutcome importHistoryFromZip(MultipartFile zipFile, Integer currentUserId, Integer currentTenantId, Integer companyId) throws Exception {
        try {
            return importHistoryFromZip(zipFile, currentUserId, currentTenantId, companyId, StandardCharsets.UTF_8);
        } catch (IllegalArgumentException e) {
            return importHistoryFromZip(zipFile, currentUserId, currentTenantId, companyId, Charset.forName("GBK"));
        }
    }

    private ImportOutcome importHistoryFromZip(MultipartFile zipFile, Integer currentUserId, Integer currentTenantId, Integer companyId, Charset charset) throws Exception {
        List<String> errorMessages = new ArrayList<>();
        int successCount = 0;
        boolean anyDataRead = false;
        Set<Integer> touchedCompanyIds = new HashSet<>();

        try (InputStream is = zipFile.getInputStream(); ZipInputStream zis = new ZipInputStream(is, charset)) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                if (entry.isDirectory()) {
                    continue;
                }
                String entryName = entry.getName();
                if (!isExcelFileName(entryName)) {
                    continue;
                }

                byte[] bytes = readAllBytes(zis);
                String entryFileName = safeFileLabel(entryName);
                MultipartFile excelFile = new InMemoryMultipartFile(entryFileName, bytes);

                try {
                    ImportOutcome outcome = importHistoryFromExcel(excelFile, entryFileName, currentUserId, currentTenantId, companyId);
                    if (outcome.anyDataRead) {
                        anyDataRead = true;
                        successCount += outcome.successCount;
                        errorMessages.addAll(outcome.errorMessages);
                        touchedCompanyIds.addAll(outcome.touchedCompanyIds);
                    }
                } catch (Exception e) {
                    errorMessages.add("【" + entryFileName + "】解析失败：" + e.getMessage());
                }
            }
        }
        return new ImportOutcome(anyDataRead, successCount, errorMessages, touchedCompanyIds);
    }

    private static boolean isZip(MultipartFile file) {
        String filename = file != null ? file.getOriginalFilename() : null;
        if (filename == null) {
            return false;
        }
        return filename.toLowerCase(Locale.ROOT).endsWith(".zip");
    }

    private ExcelImportSupport.ImportResult<CreditJudgmentDebtorImportParam> readDebtorImport(MultipartFile excelFile, boolean strictDebtorSheet) throws Exception {
        List<Integer> debtorSheetIndices = findDebtorSheetIndices(excelFile);
        for (Integer sheetIndex : debtorSheetIndices) {
            ExcelImportSupport.ImportResult<CreditJudgmentDebtorImportParam> sheetResult = ExcelImportSupport.readBest(
                excelFile,
                CreditJudgmentDebtorImportParam.class,
                this::isEmptyImportRow,
                this::isScoreImportRow,
                sheetIndex
            );
            if (!CollectionUtils.isEmpty(sheetResult.getData())) {
                return sheetResult;
            }
        }
        if (strictDebtorSheet) {
            return new ExcelImportSupport.ImportResult<>(new ArrayList<>(), 0, 0);
        }
        return ExcelImportSupport.readAnySheetBest(excelFile, CreditJudgmentDebtorImportParam.class, this::isEmptyImportRow, this::isScoreImportRow);
    }

    private boolean isScoreImportRow(CreditJudgmentDebtorImportParam param) {
        if (param == null) {
            return false;
        }
        if (isImportHeaderRow(param)) {
            return false;
        }
        return !ImportHelper.isBlank(param.getCaseNumber());
    }

    private List<Integer> findDebtorSheetIndices(MultipartFile excelFile) throws Exception {
        // Prefer an explicitly-named "被执行人" sheet when present.
        List<Integer> preferred = new ArrayList<>();
        List<Integer> indices = new ArrayList<>();
        try (InputStream is = excelFile.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) {
            int sheetCount = workbook.getNumberOfSheets();
            for (int i = 0; i < sheetCount; i++) {
                String sheetName = workbook.getSheetName(i);
                if (!isDebtorSheetName(sheetName)) {
                    continue;
                }
                String normalized = normalizeSheetName(sheetName);
                if ("被执行人".equals(normalized)) {
                    preferred.add(i);
                } else {
                    indices.add(i);
                }
            }
        }
        preferred.addAll(indices);
        return preferred;
    }

    private static boolean isDebtorSheetName(String sheetName) {
        if (sheetName == null) {
            return false;
        }
        String normalized = normalizeSheetName(sheetName);
        return normalized.contains("被执行人") && !normalized.contains("失信") && !normalized.contains("历史");
    }

    private static String normalizeSheetName(String sheetName) {
        if (sheetName == null) {
            return "";
        }
        return sheetName.replace(" ", "").replace("　", "").trim();
    }

    private static boolean isExcelFileName(String name) {
        if (name == null) {
            return false;
        }
        String lower = name.toLowerCase(Locale.ROOT);
        return lower.endsWith(".xlsx") || lower.endsWith(".xls") || lower.endsWith(".xlsm");
    }

    private static String safeFileLabel(String name) {
        if (ImportHelper.isBlank(name)) {
            return "";
        }
        int lastSlash = name.lastIndexOf('/');
        if (lastSlash >= 0 && lastSlash + 1 < name.length()) {
            return name.substring(lastSlash + 1);
        }
        return name;
    }

    private static byte[] readAllBytes(InputStream inputStream) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[8192];
        int read;
        while ((read = inputStream.read(buffer)) != -1) {
            out.write(buffer, 0, read);
        }
        return out.toByteArray();
    }

    private static class InMemoryMultipartFile implements MultipartFile {
        private final String originalFilename;
        private final byte[] bytes;

        private InMemoryMultipartFile(String originalFilename, byte[] bytes) {
            this.originalFilename = originalFilename;
            this.bytes = bytes != null ? bytes : new byte[0];
        }

        @Override
        public String getName() {
            return originalFilename;
        }

        @Override
        public String getOriginalFilename() {
            return originalFilename;
        }

        @Override
        public String getContentType() {
            return null;
        }

        @Override
        public boolean isEmpty() {
            return bytes.length == 0;
        }

        @Override
        public long getSize() {
            return bytes.length;
        }

        @Override
        public byte[] getBytes() {
            return bytes;
        }

        @Override
        public InputStream getInputStream() {
            return new java.io.ByteArrayInputStream(bytes);
        }

        @Override
        public void transferTo(java.io.File dest) throws IOException {
            Files.write(dest.toPath(), bytes);
        }
    }

}
