package com.nanri.aiimage.modules.ziniao.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.ZiniaoProperties;
import com.nanri.aiimage.modules.ziniao.memory.model.entity.ZiniaoMemoryStoreEntity;
import com.nanri.aiimage.modules.ziniao.memory.service.ZiniaoMemoryStoreService;
import com.nanri.aiimage.modules.ziniao.memory.service.ZiniaoTransientCacheService;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopIndexEntryDto;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopIndexRefreshCursorDto;
import com.nanri.aiimage.modules.ziniao.model.cache.ZiniaoShopCacheDto;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoStaffItemVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

@Service
@RequiredArgsConstructor
@Slf4j
public class ZiniaoShopIndexService {

    public static final String STATUS_ACTIVE = "ACTIVE";
    public static final String STATUS_STALE = "STALE";
    public static final String STATUS_CONFLICT = "CONFLICT";
    public static final String STATUS_PENDING = "PENDING";
    public static final String MATCH_STATUS_MATCHED = "MATCHED";
    public static final String MATCH_STATUS_PENDING = "PENDING";
    public static final String MATCH_STATUS_CONFLICT = "CONFLICT";
    public static final String MATCH_STATUS_STALE = "INDEX_STALE";

    private static final String CACHE_TYPE_SHOP_INDEX_SCOPE_SNAPSHOT = "SHOP_INDEX_SCOPE_SNAPSHOT";
    private static final String CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR = "SHOP_INDEX_REFRESH_CURSOR";
    /** 有 shopId 时唯一键，避免同一店铺因不同员工/哈希产生多行。 */
    private static final String SHOP_ENTRY_KEY_SHOP_PREFIX = "s:";
    /** 无 shopId（冲突占位等）时仍按规范化店名存一行。 */
    private static final String SHOP_ENTRY_KEY_NAME_PREFIX = "n:";
    private static final Duration DEFAULT_ENTRY_TTL = Duration.ofHours(12);
    private static final Duration DEFAULT_CURSOR_TTL = Duration.ofHours(24);
    private static final int DEFAULT_REFRESH_BATCH_SIZE = 100;
    private static final int SHOP_INDEX_LIST_LIMIT = 10000;

    private final ZiniaoMemoryStoreService ziniaoMemoryStoreService;
    private final ZiniaoTransientCacheService ziniaoTransientCacheService;
    private final ZiniaoApiKeyProvider ziniaoApiKeyProvider;
    private final ZiniaoAuthService ziniaoAuthService;
    private final ZiniaoProperties ziniaoProperties;
    private final ObjectMapper objectMapper;

    public ZiniaoShopIndexRefreshCursorDto getRefreshCursor() {
        ZiniaoShopIndexRefreshCursorDto cursor = ziniaoTransientCacheService
                .get(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global", ZiniaoShopIndexRefreshCursorDto.class)
                .orElse(null);
        if (cursor == null) {
            cursor = new ZiniaoShopIndexRefreshCursorDto();
            cursor.setScopeKey("global");
            cursor.setStatus("IDLE");
            cursor.setMessage("索引刷新任务尚未执行");
            cursor.setNextApiKeyOffset(0);
        }
        return cursor;
    }

    /**
     * @param buildOpenStoreUrl 是否拼接紫鸟打开店铺 URL；商品风险等由 Python 侧自行拼地址时应传 false，避免多余鉴权调用且不因链接失败误判匹配状态。
     */
    public ZiniaoShopMatchResultVo findIndexedStoreByName(String targetShopName, boolean buildOpenStoreUrl) {
        String normalizedShopName = normalizeShopName(targetShopName);
        if (normalizedShopName.isBlank()) {
            return pendingResult("店铺名为空，无法匹配索引");
        }

        ZiniaoShopIndexEntryDto entry = findShopIndexEntryForLookup(normalizedShopName);
        if (entry == null) {
            return pendingResult("店铺索引未命中，请等待后台刷新");
        }
        if (STATUS_CONFLICT.equals(entry.getStatus())) {
            return conflictResult(entry.getMessage() == null || entry.getMessage().isBlank()
                    ? "存在多个同名店铺，请人工确认"
                    : entry.getMessage());
        }
        if (!STATUS_ACTIVE.equals(entry.getStatus())) {
            return staleOrPendingResult(entry.getMessage() == null || entry.getMessage().isBlank()
                    ? "店铺索引暂不可用，请等待后台刷新"
                    : entry.getMessage(), entry);
        }
        if (entry.getShopId() == null || entry.getShopId().isBlank() || entry.getMatchedUserId() == null || entry.getMatchedUserId() <= 0) {
            return pendingResult("店铺索引信息不完整，请等待后台刷新");
        }

        if (!buildOpenStoreUrl) {
            ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
            vo.setMatched(true);
            vo.setShopId(entry.getShopId());
            vo.setShopName(entry.getShopName());
            vo.setCompanyName(entry.getCompanyName());
            vo.setPlatform(entry.getPlatform());
            vo.setMatchedUserId(entry.getMatchedUserId());
            vo.setOpenStoreUrl(null);
            vo.setMatchStatus(isFresh(entry) ? MATCH_STATUS_MATCHED : MATCH_STATUS_STALE);
            vo.setMatchMessage(isFresh(entry) ? null : "店铺索引已过保鲜期，请等待后台刷新");
            return vo;
        }

        String apiKey = findApiKeyByHash(entry.getApiKeyHash());
        if (apiKey == null) {
            return pendingResult("店铺索引所依赖的紫鸟 key 已失效，请等待后台刷新");
        }

        Long companyId = entry.getCompanyId();
        if (companyId == null || companyId <= 0) {
            return pendingResult("店铺索引缺少 companyId，请等待后台刷新");
        }

        String openStoreUrl = null;
        boolean openUrlFailed = false;
        try {
            openStoreUrl = ziniaoAuthService.buildOpenStoreUrlByScope(apiKey, companyId, entry.getMatchedUserId(), entry.getShopId());
        } catch (BusinessException ex) {
            openUrlFailed = true;
            log.warn("[ziniao-shop-index] buildOpenStoreUrl failed normalizedName={} msg={}", normalizedShopName, ex.getMessage());
        }
        ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
        vo.setMatched(true);
        vo.setShopId(entry.getShopId());
        vo.setShopName(entry.getShopName());
        vo.setCompanyName(entry.getCompanyName());
        vo.setPlatform(entry.getPlatform());
        vo.setMatchedUserId(entry.getMatchedUserId());
        vo.setOpenStoreUrl(openStoreUrl);
        if (openUrlFailed) {
            vo.setMatchStatus(MATCH_STATUS_STALE);
            vo.setMatchMessage("打开店铺链接暂时无法生成，请稍后重试");
        } else {
            vo.setMatchStatus(isFresh(entry) ? MATCH_STATUS_MATCHED : MATCH_STATUS_STALE);
            vo.setMatchMessage(isFresh(entry) ? null : "店铺索引已过保鲜期，请等待后台刷新");
        }
        return vo;
    }

    public ZiniaoShopMatchResultVo findIndexedStoreByName(String targetShopName) {
        return findIndexedStoreByName(targetShopName, true);
    }

    public void refreshShopIndex() {
        long now = Instant.now().toEpochMilli();
        ZiniaoShopIndexRefreshCursorDto previousCursor = ziniaoTransientCacheService
                .get(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global", ZiniaoShopIndexRefreshCursorDto.class)
                .orElse(null);
        int previousOffset = previousCursor == null || previousCursor.getNextApiKeyOffset() == null
                ? 0
                : Math.max(previousCursor.getNextApiKeyOffset(), 0);
        ZiniaoShopIndexRefreshCursorDto cursor = new ZiniaoShopIndexRefreshCursorDto();
        cursor.setScopeKey("global");
        cursor.setStatus("RUNNING");
        cursor.setLastStartedAt(now);
        cursor.setNextApiKeyOffset(previousOffset);
        ziniaoTransientCacheService.put(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global", cursor, DEFAULT_CURSOR_TTL);
        log.info("[ziniao-index] refresh started");

        Map<String, List<ZiniaoShopIndexEntryDto>> grouped = new LinkedHashMap<>();
        Map<String, Long> storesFingerprintToUserId = new LinkedHashMap<>();
        Set<Long> allInvalidUserIds = new LinkedHashSet<>();
        int refreshBatchSize = resolveRefreshBatchSize();
        try {
            List<ZiniaoApiKeyProvider.ApiKeyAccount> allApiKeyAccounts = ziniaoApiKeyProvider.listApiKeyAccounts();
            List<ZiniaoApiKeyProvider.ApiKeyAccount> apiKeyAccounts = selectApiKeyBatchByOffset(
                    allApiKeyAccounts,
                    previousOffset,
                    refreshBatchSize
            );
            int nextOffset = computeNextOffset(allApiKeyAccounts.size(), previousOffset, apiKeyAccounts.size(), refreshBatchSize);
            for (ZiniaoApiKeyProvider.ApiKeyAccount apiKeyAccount : apiKeyAccounts) {
                String apiKey = apiKeyAccount.apiKey();
                String companyName = apiKeyAccount.accountName();
                Long companyId;
                try {
                    companyId = ziniaoAuthService.resolveCompanyIdForIndex(apiKey);
                } catch (BusinessException ex) {
                    log.warn("[ziniao-index] skip apiKey while resolving companyId, msg={}", ex.getMessage());
                    continue;
                }

                Set<Long> invalidUserIds = new LinkedHashSet<>();
                List<ZiniaoStaffItemVo> staff = ziniaoAuthService.getOrLoadStaffForIndex(apiKey, companyId);
                for (Long userId : buildUserIds(staff)) {
                    if (invalidUserIds.contains(userId)) {
                        continue;
                    }
                    List<ZiniaoShopCacheDto> stores;
                    try {
                        stores = ziniaoAuthService.getOrLoadUserStoresForIndex(apiKey, companyId, userId);
                    } catch (BusinessException ex) {
                        if (ziniaoAuthService.isInvalidUserStoresError(ex)) {
                            invalidUserIds.add(userId);
                            allInvalidUserIds.add(userId);
                            ziniaoAuthService.evictInvalidUserForIndex(apiKey, companyId, userId);
                        }
                        log.warn("[ziniao-index] skip user stores, companyId={}, userId={}, msg={}", companyId, userId, ex.getMessage());
                        continue;
                    }
                    if (stores == null || stores.isEmpty()) {
                        ziniaoTransientCacheService.delete(
                                CACHE_TYPE_SHOP_INDEX_SCOPE_SNAPSHOT,
                                buildScopeSnapshotKey(apiKey, companyId, userId)
                        );
                        continue;
                    }
                    String storesFingerprint = buildStoresFingerprint(stores);
                    Long duplicatedUserId = storesFingerprintToUserId.putIfAbsent(storesFingerprint, userId);
                    if (duplicatedUserId != null) {
                        ziniaoTransientCacheService.delete(
                                CACHE_TYPE_SHOP_INDEX_SCOPE_SNAPSHOT,
                                buildScopeSnapshotKey(apiKey, companyId, userId)
                        );
                        ziniaoAuthService.evictUserStoresCacheForIndex(apiKey, companyId, userId);
                        log.info("[ziniao-index] skip duplicated user stores snapshot, companyId={}, userId={}, duplicatedUserId={}", companyId, userId, duplicatedUserId);
                        continue;
                    }
                    ziniaoTransientCacheService.put(
                            CACHE_TYPE_SHOP_INDEX_SCOPE_SNAPSHOT,
                            buildScopeSnapshotKey(apiKey, companyId, userId),
                            stores,
                            resolveEntryTtl()
                    );
                    for (ZiniaoShopCacheDto store : stores) {
                        String normalizedShopName = normalizeShopName(store == null ? null : store.getShopName());
                        if (normalizedShopName.isBlank()) {
                            continue;
                        }
                        ZiniaoShopIndexEntryDto entry = new ZiniaoShopIndexEntryDto();
                        entry.setNormalizedShopName(normalizedShopName);
                        entry.setShopId(store.getShopId());
                        entry.setShopName(store.getShopName());
                        entry.setCompanyName(companyName);
                        entry.setPlatform(store.getPlatform());
                        entry.setMatchedUserId(userId);
                        entry.setCompanyId(companyId);
                        entry.setApiKeyHash(buildApiKeyHash(apiKey));
                        entry.setStatus(STATUS_ACTIVE);
                        entry.setLastSeenAt(now);
                        entry.setLastRefreshedAt(now);
                        List<ZiniaoShopIndexEntryDto> candidates = grouped.computeIfAbsent(normalizedShopName, ignored -> new ArrayList<>());
                        if (candidates.stream().noneMatch(existing -> sameCandidate(existing, entry))) {
                            candidates.add(entry);
                        }
                    }
                }
                if (!invalidUserIds.isEmpty()) {
                    for (Long invalidUserId : invalidUserIds) {
                        ziniaoTransientCacheService.delete(
                                CACHE_TYPE_SHOP_INDEX_SCOPE_SNAPSHOT,
                                buildScopeSnapshotKey(apiKey, companyId, invalidUserId)
                        );
                    }
                }
            }

            List<ZiniaoShopIndexEntryDto> roundEntries = new ArrayList<>();
            for (Map.Entry<String, List<ZiniaoShopIndexEntryDto>> groupedEntry : grouped.entrySet()) {
                String normalizedShopName = groupedEntry.getKey();
                List<ZiniaoShopIndexEntryDto> candidates = groupedEntry.getValue();
                ZiniaoShopIndexEntryDto entryToStore = candidates.get(0);
                if (candidates.size() > 1) {
                    entryToStore = buildConflictEntry(normalizedShopName, candidates, now);
                }
                roundEntries.add(entryToStore);
            }
            Map<String, ZiniaoShopIndexEntryDto> byShopId = new LinkedHashMap<>();
            Map<String, ZiniaoShopIndexEntryDto> byNameOnly = new LinkedHashMap<>();
            for (ZiniaoShopIndexEntryDto e : roundEntries) {
                String sid = e.getShopId();
                if (sid != null && !sid.isBlank()) {
                    byShopId.merge(sid, e, this::mergeDuplicateShopIndexEntries);
                } else if (e.getNormalizedShopName() != null && !e.getNormalizedShopName().isBlank()) {
                    byNameOnly.merge(e.getNormalizedShopName(), e, this::mergeDuplicateShopIndexEntries);
                }
            }
            Set<String> activeCacheKeys = new HashSet<>();
            for (ZiniaoShopIndexEntryDto e : byShopId.values()) {
                String key = SHOP_ENTRY_KEY_SHOP_PREFIX + e.getShopId();
                activeCacheKeys.add(key);
                ziniaoMemoryStoreService.put(ZiniaoMemoryStoreService.CACHE_TYPE_SHOP_INDEX_ENTRY, key, e, resolveEntryTtl());
            }
            for (ZiniaoShopIndexEntryDto e : byNameOnly.values()) {
                String key = SHOP_ENTRY_KEY_NAME_PREFIX + e.getNormalizedShopName();
                activeCacheKeys.add(key);
                ziniaoMemoryStoreService.put(ZiniaoMemoryStoreService.CACHE_TYPE_SHOP_INDEX_ENTRY, key, e, resolveEntryTtl());
            }
            log.info("[ziniao-index] refresh persist shopIndex uniqueShopId={} nameOnly={} groupedNames={} activeRows={}",
                    byShopId.size(), byNameOnly.size(), grouped.size(), activeCacheKeys.size());
            markMissingEntriesAsStale(activeCacheKeys, now);

            cursor.setStatus("SUCCESS");
            cursor.setMessage(allInvalidUserIds.isEmpty()
                    ? null
                    : "本轮刷新已跳过无效 userId 数: " + allInvalidUserIds.size());
            cursor.setApiKeyTotal(allApiKeyAccounts.size());
            cursor.setLastProcessedApiKeyCount(apiKeyAccounts.size());
            cursor.setNextApiKeyOffset(nextOffset);
            cursor.setInvalidUserCount(allInvalidUserIds.size());
            cursor.setSampleInvalidUserIds(allInvalidUserIds.stream().limit(20).toList());
            cursor.setLastFinishedAt(now);
            cursor.setLastSuccessAt(now);
            ziniaoTransientCacheService.put(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global", cursor, DEFAULT_CURSOR_TTL);
            log.info("[ziniao-index] refresh success invalidUsersSkipped={} apiKeyProcessed={}/{} nextOffset={}",
                    allInvalidUserIds.size(), apiKeyAccounts.size(), allApiKeyAccounts.size(), nextOffset);
        } catch (Exception ex) {
            cursor.setStatus("FAILED");
            cursor.setMessage(ex.getMessage());
            cursor.setLastFinishedAt(now);
            ziniaoTransientCacheService.put(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global", cursor, DEFAULT_CURSOR_TTL);
            log.warn("[ziniao-index] refresh failed status=FAILED msg={}", ex.getMessage());
            if (ex instanceof BusinessException businessException) {
                throw businessException;
            }
            throw new BusinessException("刷新紫鸟店铺索引失败");
        }
    }

    public void invalidateIndex() {
        ziniaoTransientCacheService.delete(CACHE_TYPE_SHOP_INDEX_REFRESH_CURSOR, "global");
        log.info("[ziniao-index] cursor invalidated (transient only; shop rows unchanged)");
    }

    public String normalizeShopName(String value) {
        if (value == null) {
            return "";
        }
        return value.replace("\u3000", " ").trim();
    }

    private ZiniaoShopMatchResultVo pendingResult(String message) {
        ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
        vo.setMatched(false);
        vo.setMatchStatus(MATCH_STATUS_PENDING);
        vo.setMatchMessage(message);
        return vo;
    }

    private ZiniaoShopMatchResultVo conflictResult(String message) {
        ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
        vo.setMatched(false);
        vo.setMatchStatus(MATCH_STATUS_CONFLICT);
        vo.setMatchMessage(message);
        return vo;
    }

    private ZiniaoShopIndexEntryDto buildConflictEntry(String normalizedShopName, List<ZiniaoShopIndexEntryDto> candidates, long now) {
        ZiniaoShopIndexEntryDto representative = candidates == null || candidates.isEmpty() ? null : candidates.get(0);
        ZiniaoShopIndexEntryDto entry = new ZiniaoShopIndexEntryDto();
        entry.setNormalizedShopName(normalizedShopName);
        entry.setStatus(STATUS_CONFLICT);
        entry.setShopName(representative == null ? normalizedShopName : representative.getShopName());
        entry.setCompanyName(representative == null ? null : representative.getCompanyName());
        entry.setCompanyId(representative == null ? null : representative.getCompanyId());
        entry.setApiKeyHash(representative == null ? null : representative.getApiKeyHash());
        entry.setCandidateCount(candidates == null ? 0 : candidates.size());
        entry.setMessage("存在多个同名店铺，请人工确认（候选数: " + entry.getCandidateCount() + "）");
        entry.setLastSeenAt(now);
        entry.setLastRefreshedAt(now);
        if (candidates != null) {
            for (ZiniaoShopIndexEntryDto candidate : candidates) {
                if (candidate == null) {
                    continue;
                }
                if (candidate.getShopId() != null && !candidate.getShopId().isBlank() && !entry.getSampleShopIds().contains(candidate.getShopId())) {
                    entry.getSampleShopIds().add(candidate.getShopId());
                }
                if (candidate.getMatchedUserId() != null && candidate.getMatchedUserId() > 0 && !entry.getSampleUserIds().contains(candidate.getMatchedUserId())) {
                    entry.getSampleUserIds().add(candidate.getMatchedUserId());
                }
            }
        }
        return entry;
    }

    private ZiniaoShopMatchResultVo staleOrPendingResult(String defaultMessage, ZiniaoShopIndexEntryDto entry) {
        if (entry != null && STATUS_STALE.equals(entry.getStatus())) {
            ZiniaoShopMatchResultVo vo = new ZiniaoShopMatchResultVo();
            vo.setMatched(false);
            vo.setMatchStatus(MATCH_STATUS_STALE);
            vo.setMatchMessage(defaultMessage);
            return vo;
        }
        return pendingResult(defaultMessage);
    }

    private ZiniaoShopIndexEntryDto findShopIndexEntryForLookup(String normalizedShopName) {
        String prefixedKey = SHOP_ENTRY_KEY_NAME_PREFIX + normalizedShopName;
        ZiniaoShopIndexEntryDto entry = ziniaoMemoryStoreService
                .get(ZiniaoMemoryStoreService.CACHE_TYPE_SHOP_INDEX_ENTRY, prefixedKey, ZiniaoShopIndexEntryDto.class)
                .orElse(null);
        if (entry != null) {
            log.debug("[ziniao-shop-index] lookup hit=prefixedName cacheKey={}", prefixedKey);
            return entry;
        }
        entry = ziniaoMemoryStoreService
                .get(ZiniaoMemoryStoreService.CACHE_TYPE_SHOP_INDEX_ENTRY, normalizedShopName, ZiniaoShopIndexEntryDto.class)
                .orElse(null);
        if (entry != null) {
            log.info("[ziniao-shop-index] lookup hit=legacyKey please migrate cacheKey={}", normalizedShopName);
            return entry;
        }
        List<ZiniaoShopIndexEntryDto> rows = ziniaoMemoryStoreService.listByType(
                ZiniaoMemoryStoreService.CACHE_TYPE_SHOP_INDEX_ENTRY,
                ZiniaoShopIndexEntryDto.class,
                SHOP_INDEX_LIST_LIMIT
        );
        for (ZiniaoShopIndexEntryDto row : rows) {
            if (row == null) {
                continue;
            }
            if (normalizedShopName.equals(row.getNormalizedShopName())
                    || normalizedShopName.equals(normalizeShopName(row.getShopName()))) {
                log.info("[ziniao-shop-index] lookup hit=scanByShopName shopId={} normalizedInRow={}",
                        row.getShopId(), row.getNormalizedShopName());
                return row;
            }
        }
        log.info("[ziniao-shop-index] lookup miss normalizedName={}", normalizedShopName);
        return null;
    }

    private ZiniaoShopIndexEntryDto mergeDuplicateShopIndexEntries(ZiniaoShopIndexEntryDto a, ZiniaoShopIndexEntryDto b) {
        if (a == null) {
            return b;
        }
        if (b == null) {
            return a;
        }
        if (STATUS_CONFLICT.equals(a.getStatus()) != STATUS_CONFLICT.equals(b.getStatus())) {
            return STATUS_CONFLICT.equals(a.getStatus()) ? a : b;
        }
        if (STATUS_CONFLICT.equals(a.getStatus())) {
            int ca = a.getCandidateCount() == null ? 0 : a.getCandidateCount();
            int cb = b.getCandidateCount() == null ? 0 : b.getCandidateCount();
            return cb > ca ? b : a;
        }
        long ta = a.getLastRefreshedAt() != null ? a.getLastRefreshedAt() : (a.getLastSeenAt() != null ? a.getLastSeenAt() : 0L);
        long tb = b.getLastRefreshedAt() != null ? b.getLastRefreshedAt() : (b.getLastSeenAt() != null ? b.getLastSeenAt() : 0L);
        return tb >= ta ? b : a;
    }

    private void markMissingEntriesAsStale(Set<String> activeCacheKeys, long now) {
        List<ZiniaoMemoryStoreEntity> entities = ziniaoMemoryStoreService.listAliveEntitiesByType(
                ZiniaoMemoryStoreService.CACHE_TYPE_SHOP_INDEX_ENTRY,
                SHOP_INDEX_LIST_LIMIT
        );
        if (entities.isEmpty()) {
            return;
        }
        int staleCount = 0;
        for (ZiniaoMemoryStoreEntity entity : entities) {
            String rowKey = entity.getCacheKey();
            if (activeCacheKeys.contains(rowKey)) {
                continue;
            }
            ZiniaoShopIndexEntryDto existingEntry;
            try {
                existingEntry = objectMapper.readValue(entity.getPayloadJson(), ZiniaoShopIndexEntryDto.class);
            } catch (Exception ex) {
                log.warn("[ziniao-index] skip corrupt shop_index row cacheKey={}", rowKey);
                continue;
            }
            if (existingEntry == null || existingEntry.getNormalizedShopName() == null
                    || existingEntry.getNormalizedShopName().isBlank()) {
                continue;
            }
            String companyNameInRow = entity.getCompanyName();
            if ((existingEntry.getCompanyName() == null || existingEntry.getCompanyName().isBlank())
                    && companyNameInRow != null && !companyNameInRow.isBlank()) {
                existingEntry.setCompanyName(companyNameInRow.trim());
            }
            if (existingEntry.getCompanyName() == null || existingEntry.getCompanyName().isBlank()) {
                String accountFromKeys = resolveAccountNameByApiKeyHash(existingEntry.getApiKeyHash());
                if (accountFromKeys != null && !accountFromKeys.isBlank()) {
                    existingEntry.setCompanyName(accountFromKeys.trim());
                }
            }
            if (STATUS_CONFLICT.equals(existingEntry.getStatus())
                    && (existingEntry.getCandidateCount() == null || existingEntry.getCandidateCount() <= 0)
                    && (existingEntry.getShopName() == null || existingEntry.getShopName().isBlank())
                    && (existingEntry.getApiKeyHash() == null || existingEntry.getApiKeyHash().isBlank())
                    && (existingEntry.getCompanyId() == null || existingEntry.getCompanyId() <= 0)) {
                ziniaoMemoryStoreService.delete(ZiniaoMemoryStoreService.CACHE_TYPE_SHOP_INDEX_ENTRY, rowKey);
                log.info("[ziniao-index] removed ghost conflict shop_index row cacheKey={}", rowKey);
                continue;
            }
            existingEntry.setStatus(STATUS_STALE);
            existingEntry.setMessage("店铺索引已过期，请等待后台刷新");
            existingEntry.setLastRefreshedAt(now);
            ziniaoMemoryStoreService.put(ZiniaoMemoryStoreService.CACHE_TYPE_SHOP_INDEX_ENTRY, rowKey, existingEntry, resolveEntryTtl());
            staleCount++;
        }
        if (staleCount > 0) {
            log.info("[ziniao-index] marked stale shop_index rows count={}", staleCount);
        }
    }

    private boolean isFresh(ZiniaoShopIndexEntryDto entry) {
        if (entry == null || entry.getLastRefreshedAt() == null || entry.getLastRefreshedAt() <= 0) {
            return false;
        }
        Integer freshMinutes = ziniaoProperties.getShopIndexFreshMinutes();
        if (freshMinutes == null || freshMinutes <= 0) {
            return true;
        }
        long freshWindowMillis = Duration.ofMinutes(freshMinutes).toMillis();
        return Instant.now().toEpochMilli() - entry.getLastRefreshedAt() <= freshWindowMillis;
    }

    private Duration resolveEntryTtl() {
        Integer ttlHours = ziniaoProperties.getShopIndexEntryTtlHours();
        if (ttlHours == null || ttlHours <= 0) {
            return DEFAULT_ENTRY_TTL;
        }
        return Duration.ofHours(ttlHours);
    }

    private int resolveRefreshBatchSize() {
        Integer batchSize = ziniaoProperties.getShopIndexRefreshBatchSize();
        if (batchSize == null || batchSize <= 0) {
            return DEFAULT_REFRESH_BATCH_SIZE;
        }
        return batchSize;
    }

    private List<ZiniaoApiKeyProvider.ApiKeyAccount> selectApiKeyBatchByOffset(
            List<ZiniaoApiKeyProvider.ApiKeyAccount> allApiKeyAccounts,
            int offset,
            int refreshBatchSize) {
        if (allApiKeyAccounts == null || allApiKeyAccounts.isEmpty()) {
            return List.of();
        }
        int total = allApiKeyAccounts.size();
        if (refreshBatchSize <= 0 || refreshBatchSize >= total) {
            return new ArrayList<>(allApiKeyAccounts);
        }
        int normalizedOffset = Math.floorMod(offset, total);
        List<ZiniaoApiKeyProvider.ApiKeyAccount> selected = new ArrayList<>(refreshBatchSize);
        for (int i = 0; i < refreshBatchSize; i++) {
            selected.add(allApiKeyAccounts.get((normalizedOffset + i) % total));
        }
        return selected;
    }

    private int computeNextOffset(int totalApiKeys, int previousOffset, int processedCount, int refreshBatchSize) {
        if (totalApiKeys <= 0) {
            return 0;
        }
        int step = processedCount > 0 ? processedCount : Math.max(refreshBatchSize, 0);
        if (step <= 0) {
            return Math.floorMod(previousOffset, totalApiKeys);
        }
        return Math.floorMod(previousOffset + step, totalApiKeys);
    }

    private List<Long> buildUserIds(List<ZiniaoStaffItemVo> staff) {
        List<Long> userIds = new ArrayList<>();
        for (ZiniaoStaffItemVo item : staff) {
            if (item != null && item.getUserId() != null && item.getUserId() > 0 && !userIds.contains(item.getUserId())) {
                userIds.add(item.getUserId());
            }
        }
        return userIds;
    }

    private boolean sameCandidate(ZiniaoShopIndexEntryDto left, ZiniaoShopIndexEntryDto right) {
        if (left == null || right == null) {
            return false;
        }
        if (left.getShopId() != null && !left.getShopId().isBlank()
                && right.getShopId() != null && !right.getShopId().isBlank()) {
            // 同一 apiKey + 公司 + shopId：多员工可见同店，紫鸟 shopId 相同，保留任一员工即可
            return Objects.equals(left.getShopId(), right.getShopId())
                    && Objects.equals(left.getCompanyId(), right.getCompanyId())
                    && Objects.equals(left.getApiKeyHash(), right.getApiKeyHash());
        }
        return Objects.equals(left.getShopName(), right.getShopName())
                && Objects.equals(left.getMatchedUserId(), right.getMatchedUserId())
                && Objects.equals(left.getCompanyId(), right.getCompanyId())
                && Objects.equals(left.getApiKeyHash(), right.getApiKeyHash());
    }

    private String buildStoresFingerprint(List<ZiniaoShopCacheDto> stores) {
        if (stores == null || stores.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        stores.stream()
                .filter(Objects::nonNull)
                .sorted((left, right) -> Objects.toString(left.getShopId(), "").compareTo(Objects.toString(right.getShopId(), "")))
                .forEach(item -> sb.append(Objects.toString(item.getShopId(), ""))
                        .append('|')
                        .append(Objects.toString(item.getShopName(), ""))
                        .append('|')
                        .append(Objects.toString(item.getPlatform(), ""))
                        .append(';'));
        return sb.toString();
    }

    private String buildScopeSnapshotKey(String apiKey, Long companyId, Long userId) {
        return buildApiKeyHash(apiKey) + ":" + companyId + ":" + userId;
    }

    private String findApiKeyByHash(String apiKeyHash) {
        if (apiKeyHash == null || apiKeyHash.isBlank()) {
            return null;
        }
        for (String apiKey : ziniaoApiKeyProvider.listApiKeys()) {
            if (apiKeyHash.equals(buildApiKeyHash(apiKey))) {
                return apiKey;
            }
        }
        return null;
    }

    private String resolveAccountNameByApiKeyHash(String apiKeyHash) {
        String apiKey = findApiKeyByHash(apiKeyHash);
        if (apiKey == null) {
            return null;
        }
        for (ZiniaoApiKeyProvider.ApiKeyAccount acc : ziniaoApiKeyProvider.listApiKeyAccounts()) {
            if (apiKey.equals(acc.apiKey())) {
                return acc.accountName();
            }
        }
        return null;
    }

    private String buildApiKeyHash(String apiKey) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(apiKey.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : hash) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception ex) {
            throw new BusinessException("生成紫鸟 apiKey 缓存键失败");
        }
    }
}
