package com.gxwebsoft.credit.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.service.CreditCompanyService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;

/**
 * credit 模块 Excel 导入批处理支持：
 * - 分批 upsert（批内一次查库 + 批量 insert/update）
 * - 每批独立事务（REQUIRES_NEW），避免单次导入事务过大拖垮数据库
 */
@Component
public class BatchImportSupport {

    private final TransactionTemplate requiresNewTx;
    private static final Pattern PARTY_SPLIT_PATTERN = Pattern.compile("[,，;；、\\n\\r\\t/|]+");

    public BatchImportSupport(PlatformTransactionManager transactionManager) {
        TransactionTemplate template = new TransactionTemplate(transactionManager);
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        this.requiresNewTx = template;
    }

    public int runInNewTx(Supplier<Integer> supplier) {
        return requiresNewTx.execute(status -> supplier.get());
    }

    public static final class CompanyIdRefreshStats {
        public final boolean anyDataRead;
        public final int updated;
        public final int matched;
        public final int notFound;
        public final int ambiguous;

        private CompanyIdRefreshStats(boolean anyDataRead, int updated, int matched, int notFound, int ambiguous) {
            this.anyDataRead = anyDataRead;
            this.updated = updated;
            this.matched = matched;
            this.notFound = notFound;
            this.ambiguous = ambiguous;
        }

        public Map<String, Object> toMap() {
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("updated", updated);
            result.put("matched", matched);
            result.put("notFound", notFound);
            result.put("ambiguous", ambiguous);
            return result;
        }
    }

    /**
     * 按企业名称匹配 CreditCompany(name / matchName) 并回填 companyId。
     *
     * <p>默认仅更新 companyId 为空/0 的记录（onlyNull=true）；onlyNull=false 时会覆盖更新（仅当 companyId 不同）。</p>
     *
     * <p>注意：为避免跨租户误更新，当 currentTenantId 为空时会按记录自身 tenantId 维度匹配，
     * tenantId 为空的记录将被跳过并计入 notFound。</p>
     */
    public <T> CompanyIdRefreshStats refreshCompanyIdByCompanyName(IService<T> service,
                                                                   CreditCompanyService creditCompanyService,
                                                                   Integer currentTenantId,
                                                                   Boolean onlyNull,
                                                                   Integer limit,
                                                                   SFunction<T, Integer> idGetter,
                                                                   BiConsumer<T, Integer> idSetter,
                                                                   SFunction<T, String> nameGetter,
                                                                   SFunction<T, Integer> companyIdGetter,
                                                                   BiConsumer<T, Integer> companyIdSetter,
                                                                   SFunction<T, Boolean> hasDataGetter,
                                                                   BiConsumer<T, Boolean> hasDataSetter,
                                                                   SFunction<T, Integer> tenantIdGetter,
                                                                   Supplier<T> patchFactory) {
        // Keep existing API; delegate to the multi-column implementation.
        return refreshCompanyIdByCompanyNames(service,
            creditCompanyService,
            currentTenantId,
            onlyNull,
            limit,
            idGetter,
            idSetter,
            companyIdGetter,
            companyIdSetter,
            hasDataGetter,
            hasDataSetter,
            tenantIdGetter,
            patchFactory,
            nameGetter);
    }

    /**
     * 按多列“当事人/企业名称”匹配 CreditCompany(name / matchName) 并回填 companyId。
     *
     * <p>按传入列顺序优先匹配：原告/上诉人 &gt; 被告/被上诉人 &gt; 其他当事人/第三人等。</p>
     *
     * <p>同一列若匹配到多个不同企业则视为歧义；若最终无法得到唯一 companyId，则跳过并计入 ambiguous/notFound。</p>
     */
    @SafeVarargs
    public final <T> CompanyIdRefreshStats refreshCompanyIdByCompanyNames(IService<T> service,
                                                                          CreditCompanyService creditCompanyService,
                                                                          Integer currentTenantId,
                                                                          Boolean onlyNull,
                                                                          Integer limit,
                                                                          SFunction<T, Integer> idGetter,
                                                                          BiConsumer<T, Integer> idSetter,
                                                                          SFunction<T, Integer> companyIdGetter,
                                                                          BiConsumer<T, Integer> companyIdSetter,
                                                                          SFunction<T, Boolean> hasDataGetter,
                                                                          BiConsumer<T, Boolean> hasDataSetter,
                                                                          SFunction<T, Integer> tenantIdGetter,
                                                                          Supplier<T> patchFactory,
                                                                          SFunction<T, String>... nameGetters) {
        boolean onlyNullFlag = (onlyNull == null) || Boolean.TRUE.equals(onlyNull);

        if (nameGetters == null || nameGetters.length == 0) {
            return new CompanyIdRefreshStats(false, 0, 0, 0, 0);
        }

        // 1) 读取待处理数据（仅取必要字段，避免一次性拉全表字段）
        @SuppressWarnings({"rawtypes", "unchecked"})
        SFunction<T, ?>[] selectColumns = (SFunction<T, ?>[]) new SFunction[4 + nameGetters.length];
        int colIdx = 0;
        selectColumns[colIdx++] = idGetter;
        selectColumns[colIdx++] = companyIdGetter;
        selectColumns[colIdx++] = hasDataGetter;
        selectColumns[colIdx++] = tenantIdGetter;
        for (SFunction<T, String> ng : nameGetters) {
            selectColumns[colIdx++] = ng;
        }

        var query = service.lambdaQuery()
            .select(selectColumns)
            .eq(currentTenantId != null, tenantIdGetter, currentTenantId)
            .and(w -> {
                // Only process rows that have at least one name column populated.
                for (int i = 0; i < nameGetters.length; i++) {
                    if (i == 0) {
                        w.isNotNull(nameGetters[i]);
                    } else {
                        w.or().isNotNull(nameGetters[i]);
                    }
                }
            });
        if (onlyNullFlag) {
            // Historically some tables used 0 as the "unset" companyId, while others left it NULL.
            // Treat both as "unset" so refresh won't silently do nothing.
            query.and(w -> w.isNull(companyIdGetter).or().eq(companyIdGetter, 0));
        }
        if (limit != null && limit > 0) {
            query.last("limit " + Math.min(limit, 200000));
        }
        List<T> rows = query.list();

        if (CollectionUtils.isEmpty(rows)) {
            return new CompanyIdRefreshStats(false, 0, 0, 0, 0);
        }

        // 2) 按租户维度匹配（避免管理员/跨租户场景误匹配）
        Map<Integer, List<T>> rowsByTenant = new LinkedHashMap<>();
        int missingTenant = 0;
        for (T row : rows) {
            if (row == null) {
                continue;
            }
            Integer tenantId = currentTenantId != null ? currentTenantId : tenantIdGetter.apply(row);
            if (tenantId == null) {
                // 未知租户下不做跨租户匹配，避免误更新
                missingTenant++;
                continue;
            }
            rowsByTenant.computeIfAbsent(tenantId, k -> new ArrayList<>()).add(row);
        }

        // 3) 批量更新 companyId
        int updated = 0;
        int matched = 0;
        int notFound = 0;
        int ambiguous = 0;
        final int batchSize = 500;
        List<T> updates = new ArrayList<>(batchSize);

        final int inChunkSize = 900;
        for (Map.Entry<Integer, List<T>> entry : rowsByTenant.entrySet()) {
            Integer tenantId = entry.getKey();
            List<T> tenantRows = entry.getValue();
            if (tenantId == null || CollectionUtils.isEmpty(tenantRows)) {
                continue;
            }

            // 3.1) 查询当前租户下的 companyId 映射
            LinkedHashMap<String, Integer> companyIdByName = new LinkedHashMap<>();
            LinkedHashMap<String, Integer> ambiguousByName = new LinkedHashMap<>();
            LinkedHashSet<String> nameSet = new LinkedHashSet<>();
            for (T row : tenantRows) {
                if (row == null) {
                    continue;
                }
                for (SFunction<T, String> ng : nameGetters) {
                    for (String name : splitPartyNames(ng.apply(row))) {
                        if (name != null) {
                            nameSet.add(name);
                        }
                    }
                }
            }
            List<String> allNames = new ArrayList<>(nameSet);
            for (int i = 0; i < allNames.size(); i += inChunkSize) {
                List<String> chunk = allNames.subList(i, Math.min(allNames.size(), i + inChunkSize));
                if (CollectionUtils.isEmpty(chunk)) {
                    continue;
                }
                List<CreditCompany> companies = creditCompanyService.lambdaQuery()
                    .select(CreditCompany::getId, CreditCompany::getName, CreditCompany::getMatchName, CreditCompany::getTenantId)
                    .eq(CreditCompany::getTenantId, tenantId)
                    .and(w -> w.in(CreditCompany::getName, chunk).or().in(CreditCompany::getMatchName, chunk))
                    .list();

                for (CreditCompany c : companies) {
                    if (c == null || c.getId() == null) {
                        continue;
                    }
                    addCompanyNameMapping(companyIdByName, ambiguousByName, normalizeCompanyName(c.getName()), c.getId());
                    addCompanyNameMapping(companyIdByName, ambiguousByName, normalizeCompanyName(c.getMatchName()), c.getId());
                }
            }

            // 3.2) 更新当前租户下的数据 companyId
            for (T row : tenantRows) {
                if (row == null) {
                    continue;
                }

                Integer companyId = null;
                boolean hasAmbiguousName = false;
                for (SFunction<T, String> ng : nameGetters) {
                    LinkedHashSet<Integer> idsForColumn = new LinkedHashSet<>();
                    for (String key : splitPartyNames(ng.apply(row))) {
                        if (key == null) {
                            continue;
                        }
                        Integer amb = ambiguousByName.get(key);
                        if (amb != null && amb > 0) {
                            hasAmbiguousName = true;
                            continue;
                        }
                        Integer cid = companyIdByName.get(key);
                        if (cid != null) {
                            idsForColumn.add(cid);
                        }
                    }
                    if (idsForColumn.size() == 1) {
                        companyId = idsForColumn.iterator().next();
                        break;
                    }
                    if (idsForColumn.size() > 1) {
                        // Multiple companies matched within one column (e.g. multiple plaintiffs) -> ambiguous.
                        hasAmbiguousName = true;
                    }
                }

                if (companyId == null) {
                    if (hasAmbiguousName) {
                        ambiguous++;
                    } else {
                        notFound++;
                    }
                    continue;
                }
                matched++;

                Integer oldCompanyId = row != null ? companyIdGetter.apply(row) : null;
                Boolean oldHasData = row != null ? hasDataGetter.apply(row) : null;
                boolean needUpdate;
                if (onlyNullFlag) {
                    needUpdate = (oldCompanyId == null) || oldCompanyId == 0;
                } else {
                    needUpdate = oldCompanyId == null || !companyId.equals(oldCompanyId);
                }
                // 若已匹配到企业，但 hasData 未标记，则也需要回填 hasData=1
                if (!Boolean.TRUE.equals(oldHasData)) {
                    needUpdate = true;
                }
                if (!needUpdate) {
                    continue;
                }

                Integer id = row != null ? idGetter.apply(row) : null;
                if (id == null) {
                    continue;
                }
                T patch = patchFactory.get();
                idSetter.accept(patch, id);
                companyIdSetter.accept(patch, companyId);
                hasDataSetter.accept(patch, Boolean.TRUE);
                updates.add(patch);
                if (updates.size() >= batchSize) {
                    List<T> batch = new ArrayList<>(updates);
                    updates.clear();
                    updated += runInNewTx(() -> service.updateBatchById(batch, batchSize) ? batch.size() : 0);
                }
            }
        }

        // currentTenantId 为空时，租户缺失的数据不做匹配更新，避免误更新
        if (currentTenantId == null && missingTenant > 0) {
            notFound += missingTenant;
        }

        if (!updates.isEmpty()) {
            List<T> batch = new ArrayList<>(updates);
            updates.clear();
            updated += runInNewTx(() -> service.updateBatchById(batch, batchSize) ? batch.size() : 0);
        }

        return new CompanyIdRefreshStats(true, updated, matched, notFound, ambiguous);
    }

    /**
     * 批量 upsert：优先按 code 匹配；code 为空时按 name 匹配。
     */
    public <T> int upsertByCodeOrName(IService<T> service,
                                     List<T> items,
                                     SFunction<T, Integer> idColumn,
                                     BiConsumer<T, Integer> idSetter,
                                     SFunction<T, String> codeColumn,
                                     Function<T, String> codeGetter,
                                     SFunction<T, String> nameColumn,
                                     Function<T, String> nameGetter,
                                     Consumer<LambdaQueryWrapper<T>> extraConditions,
                                     int batchSize) {
        if (CollectionUtils.isEmpty(items)) {
            return 0;
        }

        List<String> codes = new ArrayList<>();
        List<String> names = new ArrayList<>();
        for (T item : items) {
            if (item == null) {
                continue;
            }
            String code = normalize(codeGetter.apply(item));
            if (code != null) {
                codes.add(code);
            } else {
                String name = normalize(nameGetter.apply(item));
                if (name != null) {
                    names.add(name);
                }
            }
        }

        Map<String, Integer> idByCode = new HashMap<>();
        if (!codes.isEmpty()) {
            LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
            if (extraConditions != null) {
                extraConditions.accept(wrapper);
            }
            wrapper.in(codeColumn, codes);
            wrapper.select(idColumn, codeColumn);
            for (T dbRow : service.list(wrapper)) {
                String code = normalize(codeGetter.apply(dbRow));
                Integer id = extractId(dbRow, idColumn);
                if (code != null && id != null) {
                    idByCode.putIfAbsent(code, id);
                }
            }
        }

        Map<String, Integer> idByName = new HashMap<>();
        if (!names.isEmpty()) {
            LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
            if (extraConditions != null) {
                extraConditions.accept(wrapper);
            }
            wrapper.in(nameColumn, names);
            wrapper.select(idColumn, nameColumn);
            for (T dbRow : service.list(wrapper)) {
                String name = normalize(nameGetter.apply(dbRow));
                Integer id = extractId(dbRow, idColumn);
                if (name != null && id != null) {
                    idByName.putIfAbsent(name, id);
                }
            }
        }

        List<T> updates = new ArrayList<>();
        List<T> inserts = new ArrayList<>();
        for (T item : items) {
            if (item == null) {
                continue;
            }
            String code = normalize(codeGetter.apply(item));
            Integer id = null;
            if (code != null) {
                id = idByCode.get(code);
            } else {
                String name = normalize(nameGetter.apply(item));
                if (name != null) {
                    id = idByName.get(name);
                }
            }

            if (id != null) {
                idSetter.accept(item, id);
                updates.add(item);
            } else {
                inserts.add(item);
            }
        }

        if (!updates.isEmpty()) {
            service.updateBatchById(updates, batchSize);
        }
        if (!inserts.isEmpty()) {
            service.saveBatch(inserts, batchSize);
        }
        return updates.size() + inserts.size();
    }

    /**
     * 批量 upsert：按单字段 key 匹配（key 非空）。
     */
    public <T> int upsertBySingleKey(IService<T> service,
                                    List<T> items,
                                    SFunction<T, Integer> idColumn,
                                    BiConsumer<T, Integer> idSetter,
                                    SFunction<T, String> keyColumn,
                                    Function<T, String> keyGetter,
                                    Consumer<LambdaQueryWrapper<T>> extraConditions,
                                    int batchSize) {
        if (CollectionUtils.isEmpty(items)) {
            return 0;
        }

        List<String> keys = new ArrayList<>(items.size());
        for (T item : items) {
            if (item == null) {
                continue;
            }
            String key = normalize(keyGetter.apply(item));
            if (key != null) {
                keys.add(key);
            }
        }

        Map<String, Integer> idByKey = new HashMap<>();
        if (!keys.isEmpty()) {
            LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
            if (extraConditions != null) {
                extraConditions.accept(wrapper);
            }
            wrapper.in(keyColumn, keys);
            wrapper.select(idColumn, keyColumn);
            for (T dbRow : service.list(wrapper)) {
                String key = normalize(keyGetter.apply(dbRow));
                Integer id = extractId(dbRow, idColumn);
                if (key != null && id != null) {
                    idByKey.putIfAbsent(key, id);
                }
            }
        }

        List<T> updates = new ArrayList<>();
        List<T> inserts = new ArrayList<>();
        for (T item : items) {
            if (item == null) {
                continue;
            }
            String key = normalize(keyGetter.apply(item));
            Integer id = key != null ? idByKey.get(key) : null;
            if (id != null) {
                idSetter.accept(item, id);
                updates.add(item);
            } else {
                inserts.add(item);
            }
        }

        if (!updates.isEmpty()) {
            service.updateBatchById(updates, batchSize);
        }
        if (!inserts.isEmpty()) {
            service.saveBatch(inserts, batchSize);
        }
        return updates.size() + inserts.size();
    }

    /**
     * 批量 upsert：按单字段 key 匹配（key 非空）。当匹配到已存在记录时：
     * - 覆盖更新
     * - 将 counter（通常是 recommend）在数据库原值基础上 +1，用于记录“被更新次数”
     *
     * <p>注意：counter 会被覆盖写入（不是 SQL 自增），因此该方法适合导入场景。</p>
     */
    public <T> int upsertBySingleKeyAndIncrementCounterOnUpdate(IService<T> service,
                                                                List<T> items,
                                                                SFunction<T, Integer> idColumn,
                                                                BiConsumer<T, Integer> idSetter,
                                                                SFunction<T, String> keyColumn,
                                                                Function<T, String> keyGetter,
                                                                SFunction<T, Integer> counterColumn,
                                                                BiConsumer<T, Integer> counterSetter,
                                                                Consumer<LambdaQueryWrapper<T>> extraConditions,
                                                                int batchSize) {
        if (CollectionUtils.isEmpty(items)) {
            return 0;
        }

        List<String> keys = new ArrayList<>(items.size());
        for (T item : items) {
            if (item == null) {
                continue;
            }
            String key = normalize(keyGetter.apply(item));
            if (key != null) {
                keys.add(key);
            }
        }

        Map<String, Integer> idByKey = new HashMap<>();
        Map<String, Integer> counterByKey = new HashMap<>();
        if (!keys.isEmpty()) {
            LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
            if (extraConditions != null) {
                extraConditions.accept(wrapper);
            }
            wrapper.in(keyColumn, keys);
            wrapper.select(idColumn, keyColumn, counterColumn);
            for (T dbRow : service.list(wrapper)) {
                String key = normalize(keyGetter.apply(dbRow));
                Integer id = extractId(dbRow, idColumn);
                if (key == null || id == null) {
                    continue;
                }
                idByKey.putIfAbsent(key, id);
                if (counterColumn != null) {
                    counterByKey.putIfAbsent(key, counterColumn.apply(dbRow));
                }
            }
        }

        List<T> updates = new ArrayList<>();
        List<T> inserts = new ArrayList<>();
        for (T item : items) {
            if (item == null) {
                continue;
            }
            String key = normalize(keyGetter.apply(item));
            Integer id = key != null ? idByKey.get(key) : null;
            if (id != null) {
                idSetter.accept(item, id);
                Integer old = key != null ? counterByKey.get(key) : null;
                if (counterSetter != null) {
                    counterSetter.accept(item, old == null ? 1 : old + 1);
                }
                updates.add(item);
            } else {
                // insert：如果未提供 counterSetter，则不做处理；如果提供则默认 0。
                if (counterSetter != null) {
                    counterSetter.accept(item, 0);
                }
                inserts.add(item);
            }
        }

        if (!updates.isEmpty()) {
            service.updateBatchById(updates, batchSize);
        }
        if (!inserts.isEmpty()) {
            service.saveBatch(inserts, batchSize);
        }
        return updates.size() + inserts.size();
    }

    /**
     * 批量 upsert：优先按 code 匹配；code 为空时按 name 匹配。匹配到已存在记录时 counter +1。
     */
    public <T> int upsertByCodeOrNameAndIncrementCounterOnUpdate(IService<T> service,
                                                                 List<T> items,
                                                                 SFunction<T, Integer> idColumn,
                                                                 BiConsumer<T, Integer> idSetter,
                                                                 SFunction<T, String> codeColumn,
                                                                 Function<T, String> codeGetter,
                                                                 SFunction<T, String> nameColumn,
                                                                 Function<T, String> nameGetter,
                                                                 SFunction<T, Integer> counterColumn,
                                                                 BiConsumer<T, Integer> counterSetter,
                                                                 Consumer<LambdaQueryWrapper<T>> extraConditions,
                                                                 int batchSize) {
        if (CollectionUtils.isEmpty(items)) {
            return 0;
        }

        List<String> codes = new ArrayList<>();
        List<String> names = new ArrayList<>();
        for (T item : items) {
            if (item == null) {
                continue;
            }
            String code = normalize(codeGetter.apply(item));
            if (code != null) {
                codes.add(code);
            } else {
                String name = normalize(nameGetter.apply(item));
                if (name != null) {
                    names.add(name);
                }
            }
        }

        Map<String, Integer> idByCode = new HashMap<>();
        Map<String, Integer> counterByCode = new HashMap<>();
        if (!codes.isEmpty()) {
            LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
            if (extraConditions != null) {
                extraConditions.accept(wrapper);
            }
            wrapper.in(codeColumn, codes);
            wrapper.select(idColumn, codeColumn, counterColumn);
            for (T dbRow : service.list(wrapper)) {
                String code = normalize(codeGetter.apply(dbRow));
                Integer id = extractId(dbRow, idColumn);
                if (code == null || id == null) {
                    continue;
                }
                idByCode.putIfAbsent(code, id);
                if (counterColumn != null) {
                    counterByCode.putIfAbsent(code, counterColumn.apply(dbRow));
                }
            }
        }

        Map<String, Integer> idByName = new HashMap<>();
        Map<String, Integer> counterByName = new HashMap<>();
        if (!names.isEmpty()) {
            LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
            if (extraConditions != null) {
                extraConditions.accept(wrapper);
            }
            wrapper.in(nameColumn, names);
            wrapper.select(idColumn, nameColumn, counterColumn);
            for (T dbRow : service.list(wrapper)) {
                String name = normalize(nameGetter.apply(dbRow));
                Integer id = extractId(dbRow, idColumn);
                if (name == null || id == null) {
                    continue;
                }
                idByName.putIfAbsent(name, id);
                if (counterColumn != null) {
                    counterByName.putIfAbsent(name, counterColumn.apply(dbRow));
                }
            }
        }

        List<T> updates = new ArrayList<>();
        List<T> inserts = new ArrayList<>();
        for (T item : items) {
            if (item == null) {
                continue;
            }
            String code = normalize(codeGetter.apply(item));
            Integer id = null;
            Integer old = null;
            if (code != null) {
                id = idByCode.get(code);
                old = counterByCode.get(code);
            } else {
                String name = normalize(nameGetter.apply(item));
                if (name != null) {
                    id = idByName.get(name);
                    old = counterByName.get(name);
                }
            }

            if (id != null) {
                idSetter.accept(item, id);
                if (counterSetter != null) {
                    counterSetter.accept(item, old == null ? 1 : old + 1);
                }
                updates.add(item);
            } else {
                if (counterSetter != null) {
                    counterSetter.accept(item, 0);
                }
                inserts.add(item);
            }
        }

        if (!updates.isEmpty()) {
            service.updateBatchById(updates, batchSize);
        }
        if (!inserts.isEmpty()) {
            service.saveBatch(inserts, batchSize);
        }
        return updates.size() + inserts.size();
    }

    /**
     * 批量失败时降级逐行，尽量保留“第 N 行”错误定位。
     */
    public <T> int persistChunkWithFallback(List<T> items,
                                           List<Integer> excelRowNumbers,
                                           Supplier<Integer> batchPersist,
                                           BiFunction<T, Integer, Boolean> rowPersist,
                                           List<String> errorMessages) {
        if (CollectionUtils.isEmpty(items)) {
            return 0;
        }
        try {
            return runInNewTx(batchPersist);
        } catch (Exception batchException) {
            int successCount = 0;
            for (int i = 0; i < items.size(); i++) {
                T item = items.get(i);
                int excelRowNumber = (excelRowNumbers != null && i < excelRowNumbers.size()) ? excelRowNumbers.get(i) : -1;
                try {
                    int delta = runInNewTx(() -> rowPersist.apply(item, excelRowNumber) ? 1 : 0);
                    successCount += delta;
                } catch (Exception e) {
                    if (errorMessages != null) {
                        if (excelRowNumber > 0) {
                            errorMessages.add("第" + excelRowNumber + "行：" + e.getMessage());
                        } else {
                            errorMessages.add(e.getMessage());
                        }
                    }
                }
            }
            return successCount;
        }
    }

    /**
     * 批量失败时降级逐行：允许调用方自定义“成功条数”的计算口径（例如：仅统计 insert 入库条数）。
     *
     * <p>batchPersistCount / rowPersistCount 返回的是“需要累计的条数增量”，并不等同于“是否成功”。</p>
     */
    public <T> int persistChunkWithFallbackCount(List<T> items,
                                                List<Integer> excelRowNumbers,
                                                Supplier<Integer> batchPersistCount,
                                                BiFunction<T, Integer, Integer> rowPersistCount,
                                                List<String> errorMessages) {
        if (CollectionUtils.isEmpty(items)) {
            return 0;
        }
        try {
            return runInNewTx(batchPersistCount);
        } catch (Exception batchException) {
            int count = 0;
            for (int i = 0; i < items.size(); i++) {
                T item = items.get(i);
                int excelRowNumber = (excelRowNumbers != null && i < excelRowNumbers.size()) ? excelRowNumbers.get(i) : -1;
                try {
                    Integer delta = runInNewTx(() -> rowPersistCount.apply(item, excelRowNumber));
                    if (delta != null && delta > 0) {
                        count += delta;
                    }
                } catch (Exception e) {
                    if (errorMessages != null) {
                        if (excelRowNumber > 0) {
                            errorMessages.add("第" + excelRowNumber + "行：" + e.getMessage());
                        } else {
                            errorMessages.add(e.getMessage());
                        }
                    }
                }
            }
            return count;
        }
    }

    private static String normalize(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    private static String normalizeCompanyName(String name) {
        if (name == null) {
            return null;
        }
        // 兼容 Excel/网页复制带来的全角空格
        String v = name.replace('　', ' ').trim();
        return v.isEmpty() ? null : v;
    }

    /**
     * Split a "party names" cell into normalized company name candidates.
     * Supports common separators used in Excel/web copy (comma/semicolon/Chinese list delimiter/newlines).
     */
    private static List<String> splitPartyNames(String raw) {
        List<String> result = new ArrayList<>();
        String v = normalizeCompanyName(raw);
        if (v == null) {
            return result;
        }
        String[] parts = PARTY_SPLIT_PATTERN.split(v);
        if (parts == null || parts.length == 0) {
            result.add(v);
            return result;
        }
        for (String p : parts) {
            String item = normalizeCompanyName(p);
            if (item != null) {
                result.add(item);
            }
        }
        return result;
    }

    private static void addCompanyNameMapping(Map<String, Integer> idByName,
                                              Map<String, Integer> ambiguousByName,
                                              String key,
                                              Integer companyId) {
        if (key == null || companyId == null) {
            return;
        }
        Integer existing = idByName.get(key);
        if (existing == null) {
            idByName.put(key, companyId);
            return;
        }
        if (!existing.equals(companyId)) {
            ambiguousByName.put(key, 1);
        }
    }

    private static <T> Integer extractId(T entity, SFunction<T, Integer> idColumn) {
        // SFunction 是 getter method ref，直接调用即可
        return idColumn.apply(entity);
    }
}
