package com.gxwebsoft.credit.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.toolkit.SqlRunner;
import com.gxwebsoft.credit.entity.CreditCompany;
import com.gxwebsoft.credit.service.CreditCompanyService;
import org.springframework.dao.DataIntegrityViolationException;
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.io.Serializable;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
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());
    }

    /**
     * 硬删除（物理删除），用于替代 MyBatis-Plus 的 @TableLogic 逻辑删除。
     *
     * <p>注意：SQL 仍会经过 MyBatis-Plus 拦截器（如 TenantLine），用于保证租户隔离。</p>
     */
    public boolean hardRemoveById(Class<?> entityClass, Serializable id) {
        if (entityClass == null || id == null) {
            return false;
        }
        TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
        if (tableInfo == null) {
            throw new IllegalArgumentException("MyBatis-Plus TableInfo not found for entityClass=" + entityClass.getName());
        }
        String tableName = tableInfo.getTableName();
        String keyColumn = tableInfo.getKeyColumn();
        if (keyColumn == null || keyColumn.trim().isEmpty()) {
            keyColumn = "id";
        }
        String sql = "DELETE FROM " + tableName + " WHERE " + keyColumn + " = {0}";
        return SqlRunner.db().delete(sql, id);
    }

    /**
     * 硬删除（物理删除）- 批量
     */
    public boolean hardRemoveByIds(Class<?> entityClass, List<? extends Serializable> ids) {
        if (entityClass == null || CollectionUtils.isEmpty(ids)) {
            return false;
        }
        TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
        if (tableInfo == null) {
            throw new IllegalArgumentException("MyBatis-Plus TableInfo not found for entityClass=" + entityClass.getName());
        }
        String tableName = tableInfo.getTableName();
        String keyColumn = tableInfo.getKeyColumn();
        if (keyColumn == null || keyColumn.trim().isEmpty()) {
            keyColumn = "id";
        }

        // Keep IN-list size under common DB/driver limits.
        final int chunkSize = 900;
        boolean anyDeleted = false;
        for (int i = 0; i < ids.size(); i += chunkSize) {
            List<? extends Serializable> chunk = ids.subList(i, Math.min(ids.size(), i + chunkSize));
            if (CollectionUtils.isEmpty(chunk)) {
                continue;
            }
            StringBuilder inPlaceholders = new StringBuilder();
            for (int j = 0; j < chunk.size(); j++) {
                if (j > 0) {
                    inPlaceholders.append(",");
                }
                inPlaceholders.append("{").append(j).append("}");
            }
            String sql = "DELETE FROM " + tableName + " WHERE " + keyColumn + " IN (" + inPlaceholders + ")";
            anyDeleted = SqlRunner.db().delete(sql, chunk.toArray()) || anyDeleted;
        }
        return anyDeleted;
    }

    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 + companyName。
     *
     * <p>companyNameSetter 为空时等价于仅回填 companyId。</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,
                                                                   BiConsumer<T, String> companyNameSetter,
                                                                   SFunction<T, Boolean> hasDataGetter,
                                                                   BiConsumer<T, Boolean> hasDataSetter,
                                                                   SFunction<T, Integer> tenantIdGetter,
                                                                   Supplier<T> patchFactory) {
        return refreshCompanyIdByCompanyNames(service,
            creditCompanyService,
            currentTenantId,
            onlyNull,
            limit,
            idGetter,
            idSetter,
            companyIdGetter,
            companyIdSetter,
            companyNameSetter,
            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) {
        return refreshCompanyIdByCompanyNames(service,
            creditCompanyService,
            currentTenantId,
            onlyNull,
            limit,
            idGetter,
            idSetter,
            companyIdGetter,
            companyIdSetter,
            null,
            hasDataGetter,
            hasDataSetter,
            tenantIdGetter,
            patchFactory,
            nameGetters);
    }

    /**
     * 按多列“当事人/企业名称”匹配 CreditCompany(name / matchName) 并回填 companyId + companyName。
     *
     * <p>companyNameSetter 为空时等价于仅回填 companyId。</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,
                                                                          BiConsumer<T, String> companyNameSetter,
                                                                          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<>();
            // For display: prefer matchName (normalized) then name.
            HashMap<Integer, String> companyNameById = new HashMap<>();
            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;
                    }
                    String displayName = c.getMatchName();
                    if (displayName == null || displayName.trim().isEmpty()) {
                        displayName = c.getName();
                    }
                    if (displayName != null && !displayName.trim().isEmpty()) {
                        companyNameById.putIfAbsent(c.getId(), displayName.trim());
                    }
                    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);
                if (companyNameSetter != null) {
                    String companyName = companyNameById.get(companyId);
                    if (companyName != null && !companyName.trim().isEmpty()) {
                        companyNameSetter.accept(patch, companyName.trim());
                    }
                }
                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);
    }

    /**
     * 按“文本字段包含企业名称”的方式匹配 CreditCompany(name / matchName) 并回填 companyId。
     *
     * <p>适用场景：某些表的“当事人/第三人”字段会包含多个角色+姓名/企业，例如：
     * 申请执行人 - 张三 被执行人 - 某某有限公司。此时无法按整格等值匹配。</p>
     *
     * <p>实现：按租户加载企业 name/matchName 构建多模式匹配（AC 自动机），在文本中查找出现的企业名。</p>
     *
     * <p>列优先级：按 textGetters 的顺序尝试；若某列匹配到唯一企业则采用，否则继续下一列。</p>
     */
    @SafeVarargs
    public final <T> CompanyIdRefreshStats refreshCompanyIdByCompanyNameContainedInText(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>... textGetters) {
        return refreshCompanyIdByCompanyNameContainedInText(
            service,
            creditCompanyService,
            currentTenantId,
            onlyNull,
            limit,
            idGetter,
            idSetter,
            companyIdGetter,
            companyIdSetter,
            hasDataGetter,
            hasDataSetter,
            tenantIdGetter,
            patchFactory,
            null,
            textGetters
        );
    }

    /**
     * refreshCompanyIdByCompanyNameContainedInText 的增强版：支持对 CreditCompany 查询追加条件（例如仅匹配一级企业）。
     */
    @SafeVarargs
    public final <T> CompanyIdRefreshStats refreshCompanyIdByCompanyNameContainedInText(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,
                                                                                        Consumer<LambdaQueryChainWrapper<CreditCompany>> companyQueryCustomizer,
                                                                                        SFunction<T, String>... textGetters) {
        boolean onlyNullFlag = (onlyNull == null) || Boolean.TRUE.equals(onlyNull);
        if (textGetters == null || textGetters.length == 0) {
            return new CompanyIdRefreshStats(false, 0, 0, 0, 0);
        }

        // 1) 读取待处理数据（仅取必要字段）
        @SuppressWarnings({"rawtypes", "unchecked"})
        SFunction<T, ?>[] selectColumns = (SFunction<T, ?>[]) new SFunction[4 + textGetters.length];
        int colIdx = 0;
        selectColumns[colIdx++] = idGetter;
        selectColumns[colIdx++] = companyIdGetter;
        selectColumns[colIdx++] = hasDataGetter;
        selectColumns[colIdx++] = tenantIdGetter;
        for (SFunction<T, String> tg : textGetters) {
            selectColumns[colIdx++] = tg;
        }

        var query = service.lambdaQuery()
            .select(selectColumns)
            .eq(currentTenantId != null, tenantIdGetter, currentTenantId)
            .and(w -> {
                for (int i = 0; i < textGetters.length; i++) {
                    if (i == 0) {
                        w.isNotNull(textGetters[i]);
                    } else {
                        w.or().isNotNull(textGetters[i]);
                    }
                }
            });
        if (onlyNullFlag) {
            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);
        }

        int updated = 0;
        int matched = 0;
        int notFound = 0;
        int ambiguous = 0;
        final int batchSize = 500;
        List<T> updates = new ArrayList<>(batchSize);

        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;
            }

            // 2.1) 构建当前租户的企业名匹配器
            LambdaQueryChainWrapper<CreditCompany> companyQuery = creditCompanyService.lambdaQuery()
                .select(CreditCompany::getId, CreditCompany::getName, CreditCompany::getMatchName, CreditCompany::getTenantId)
                .eq(CreditCompany::getTenantId, tenantId)
                ;
            if (companyQueryCustomizer != null) {
                companyQueryCustomizer.accept(companyQuery);
            }
            List<CreditCompany> companies = companyQuery.list();
            CompanyNameMatcher matcher = CompanyNameMatcher.build(companies);

            // 2.2) 匹配并回填
            for (T row : tenantRows) {
                if (row == null) {
                    continue;
                }

                Integer resolvedCompanyId = null;
                boolean hasAmbiguous = false;
                for (SFunction<T, String> tg : textGetters) {
                    String text = tg.apply(row);
                    CompanyNameMatcher.MatchResult r = matcher.match(text);
                    if (r.ambiguous) {
                        hasAmbiguous = true;
                        continue;
                    }
                    if (r.companyId != null) {
                        resolvedCompanyId = r.companyId;
                        break;
                    }
                }

                if (resolvedCompanyId == null) {
                    if (hasAmbiguous) {
                        ambiguous++;
                    } else {
                        notFound++;
                    }
                    continue;
                }
                matched++;

                Integer oldCompanyId = companyIdGetter.apply(row);
                Boolean oldHasData = hasDataGetter.apply(row);
                boolean needUpdate;
                if (onlyNullFlag) {
                    needUpdate = (oldCompanyId == null) || oldCompanyId == 0;
                } else {
                    needUpdate = oldCompanyId == null || !resolvedCompanyId.equals(oldCompanyId);
                }
                if (!Boolean.TRUE.equals(oldHasData)) {
                    needUpdate = true;
                }
                if (!needUpdate) {
                    continue;
                }

                Integer id = idGetter.apply(row);
                if (id == null) {
                    continue;
                }
                T patch = patchFactory.get();
                idSetter.accept(patch, id);
                companyIdSetter.accept(patch, resolvedCompanyId);
                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);
                }
            }
        }

        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-only batch persist with "unique index" duplicate handling:
     * - try saveBatch
     * - on failure, fallback to row-by-row save
     * - if a row hits duplicate-key constraint, add a friendly error and continue
     *
     * <p>This intentionally does NOT query the database for dedup/upsert.</p>
     */
    public <T> int persistInsertOnlyChunk(IService<T> service,
                                         List<T> items,
                                         List<Integer> excelRowNumbers,
                                         int batchSize,
                                         Function<T, String> rowLabelGetter,
                                         String messagePrefix,
                                         List<String> errorMessages) {
        String prefix = messagePrefix == null ? "" : messagePrefix;
        return persistChunkWithFallback(
            items,
            excelRowNumbers,
            () -> {
                boolean ok = service.saveBatch(items, batchSize);
                if (!ok) {
                    throw new RuntimeException("批量保存失败");
                }
                return items.size();
            },
            (rowItem, rowNumber) -> {
                try {
                    boolean ok = service.save(rowItem);
                    if (!ok) {
                        if (errorMessages != null) {
                            String p = (rowNumber != null && rowNumber > 0) ? (prefix + "第" + rowNumber + "行：") : prefix;
                            errorMessages.add(p + "保存失败");
                        }
                        return false;
                    }
                    return true;
                } catch (DataIntegrityViolationException e) {
                    if (!isDuplicateKey(e)) {
                        throw e;
                    }
                    if (errorMessages != null) {
                        String label = null;
                        if (rowLabelGetter != null && rowItem != null) {
                            try {
                                label = rowLabelGetter.apply(rowItem);
                            } catch (Exception ignore) {
                                // ignore label extraction failures
                            }
                        }
                        if (label != null) {
                            label = label.trim();
                        }
                        String what = (label == null || label.isEmpty()) ? "数据" : ("【" + label + "】");
                        String p = (rowNumber != null && rowNumber > 0) ? (prefix + "第" + rowNumber + "行：") : prefix;
                        errorMessages.add(p + what + "重复（唯一索引冲突）");
                    }
                    return false;
                }
            },
            errorMessages
        );
    }

    /**
     * Best-effort duplicate-key detection across common drivers.
     */
    public static boolean isDuplicateKey(DataIntegrityViolationException e) {
        for (Throwable t = e; t != null; t = t.getCause()) {
            if (t instanceof SQLException) {
                SQLException se = (SQLException) t;
                // MySQL: 1062 Duplicate entry; PostgreSQL/H2: SQLState 23505 unique_violation
                if (se.getErrorCode() == 1062) {
                    return true;
                }
                if ("23505".equals(se.getSQLState())) {
                    return true;
                }
            }
        }
        Throwable mostSpecificCause = e.getMostSpecificCause();
        String message = mostSpecificCause != null ? mostSpecificCause.getMessage() : e.getMessage();
        if (message == null) {
            return false;
        }
        String lower = message.toLowerCase();
        return lower.contains("duplicate") && lower.contains("key");
    }

    /**
     * 批量失败时降级逐行：允许调用方自定义“成功条数”的计算口径（例如：仅统计 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);
    }

    /**
     * Multi-pattern substring matcher for company names (CreditCompany.name / matchName).
     * Uses an Aho–Corasick automaton to scan each text only once.
     */
    private static final class CompanyNameMatcher {
        private static final int MIN_PATTERN_LEN = 4; // Avoid false positives in free text (e.g. person names)

        private static final class Node {
            final Map<Character, Integer> next = new HashMap<>();
            final List<Integer> out = new ArrayList<>();
            int fail = 0;
        }

        private final List<Node> nodes;
        private final int[] patternCompanyId; // 0 means ambiguous
        private final int[] patternLen;

        private CompanyNameMatcher(List<Node> nodes, int[] patternCompanyId, int[] patternLen) {
            this.nodes = nodes;
            this.patternCompanyId = patternCompanyId;
            this.patternLen = patternLen;
        }

        static CompanyNameMatcher build(List<CreditCompany> companies) {
            List<Node> nodes = new ArrayList<>();
            nodes.add(new Node()); // root

            Map<String, Integer> patternIndex = new HashMap<>();
            List<Integer> companyIds = new ArrayList<>();
            List<Integer> patternLens = new ArrayList<>();

            if (!CollectionUtils.isEmpty(companies)) {
                for (CreditCompany c : companies) {
                    if (c == null || c.getId() == null) {
                        continue;
                    }
                    addPattern(nodes, patternIndex, companyIds, patternLens, normalizeCompanyName(c.getName()), c.getId());
                    addPattern(nodes, patternIndex, companyIds, patternLens, normalizeCompanyName(c.getMatchName()), c.getId());
                }
            }

            int[] patternCompanyId = new int[companyIds.size()];
            for (int i = 0; i < companyIds.size(); i++) {
                patternCompanyId[i] = companyIds.get(i) != null ? companyIds.get(i) : 0;
            }
            int[] patternLen = new int[patternLens.size()];
            for (int i = 0; i < patternLens.size(); i++) {
                patternLen[i] = patternLens.get(i) != null ? patternLens.get(i) : 0;
            }

            buildFailureLinks(nodes);
            return new CompanyNameMatcher(nodes, patternCompanyId, patternLen);
        }

        private static void addPattern(List<Node> nodes,
                                       Map<String, Integer> patternIndex,
                                       List<Integer> companyIds,
                                       List<Integer> patternLens,
                                       String pattern,
                                       Integer companyId) {
            if (pattern == null || companyId == null) {
                return;
            }
            if (pattern.length() < MIN_PATTERN_LEN) {
                return;
            }

            Integer existingIndex = patternIndex.get(pattern);
            if (existingIndex != null) {
                // Same pattern maps to multiple companies -> mark ambiguous.
                Integer oldCompanyId = companyIds.get(existingIndex);
                if (oldCompanyId != null && !oldCompanyId.equals(companyId)) {
                    companyIds.set(existingIndex, null);
                }
                return;
            }

            int state = 0;
            for (int i = 0; i < pattern.length(); i++) {
                char ch = pattern.charAt(i);
                Integer next = nodes.get(state).next.get(ch);
                if (next == null) {
                    next = nodes.size();
                    nodes.get(state).next.put(ch, next);
                    nodes.add(new Node());
                }
                state = next;
            }
            int idx = companyIds.size();
            companyIds.add(companyId);
            patternLens.add(pattern.length());
            nodes.get(state).out.add(idx);
            patternIndex.put(pattern, idx);
        }

        private static void buildFailureLinks(List<Node> nodes) {
            Queue<Integer> q = new ArrayDeque<>();
            // Init depth-1 nodes
            for (Map.Entry<Character, Integer> e : nodes.get(0).next.entrySet()) {
                int s = e.getValue();
                nodes.get(s).fail = 0;
                q.add(s);
            }
            while (!q.isEmpty()) {
                int r = q.poll();
                for (Map.Entry<Character, Integer> e : nodes.get(r).next.entrySet()) {
                    char a = e.getKey();
                    int s = e.getValue();
                    q.add(s);

                    int state = nodes.get(r).fail;
                    while (state != 0 && !nodes.get(state).next.containsKey(a)) {
                        state = nodes.get(state).fail;
                    }
                    Integer fs = nodes.get(state).next.get(a);
                    nodes.get(s).fail = (fs != null) ? fs : 0;
                    // Merge outputs from fail state
                    List<Integer> out = nodes.get(nodes.get(s).fail).out;
                    if (!out.isEmpty()) {
                        nodes.get(s).out.addAll(out);
                    }
                }
            }
        }

        static final class MatchResult {
            final Integer companyId; // unique match
            final boolean ambiguous;

            MatchResult(Integer companyId, boolean ambiguous) {
                this.companyId = companyId;
                this.ambiguous = ambiguous;
            }
        }

        MatchResult match(String text) {
            String v = normalizeCompanyName(text);
            if (v == null) {
                return new MatchResult(null, false);
            }

            int state = 0;
            Integer bestCompanyId = null;
            int bestStart = Integer.MAX_VALUE;
            int bestLen = -1;
            boolean ambiguous = false;
            for (int i = 0; i < v.length(); i++) {
                char ch = v.charAt(i);
                while (state != 0 && !nodes.get(state).next.containsKey(ch)) {
                    state = nodes.get(state).fail;
                }
                Integer next = nodes.get(state).next.get(ch);
                state = next != null ? next : 0;

                List<Integer> out = nodes.get(state).out;
                if (out.isEmpty()) {
                    continue;
                }
                for (Integer idx : out) {
                    if (idx == null || idx < 0 || idx >= patternCompanyId.length) {
                        continue;
                    }
                    int cid = patternCompanyId[idx];
                    // Pattern exists but maps to multiple companies -> ignore this hit, keep looking for a unique one.
                    if (cid == 0) {
                        continue;
                    }
                    int len = (idx < patternLen.length) ? patternLen[idx] : 0;
                    int start = len > 0 ? (i - len + 1) : i;
                    if (bestCompanyId == null) {
                        bestCompanyId = cid;
                        bestStart = start;
                        bestLen = len;
                        continue;
                    }
                    if (start < bestStart) {
                        bestCompanyId = cid;
                        bestStart = start;
                        bestLen = len;
                        continue;
                    }
                    if (start == bestStart) {
                        // Prefer the longer (more specific) match at the same position.
                        if (len > bestLen) {
                            bestCompanyId = cid;
                            bestLen = len;
                            continue;
                        }
                        // Same position + same length but different companyId -> truly ambiguous.
                        if (len == bestLen && !bestCompanyId.equals(cid)) {
                            ambiguous = true;
                            break;
                        }
                    }
                }
                if (ambiguous) {
                    break;
                }
            }

            if (ambiguous) {
                return new MatchResult(null, true);
            }
            return new MatchResult(bestCompanyId, false);
        }
    }
}
