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

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.DeleteBrandProgressProperties;
import com.nanri.aiimage.modules.deletebrand.model.cache.DeleteBrandParsedFileCacheDto;
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandCountryResultItemDto;
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandProcessedCountryDto;
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandResultFileDto;
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandRunRequest;
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSourceFileDto;
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSubmitResultRequest;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandCountryAsinVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandCountryGroupVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandHistoryVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandLineProgressInfoVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandLineProgressVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandPreviewRowVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandResultItemVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandRunVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskDeletionStatusVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskDetailVo;
import com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskItemVo;
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

@Service
@RequiredArgsConstructor
@Slf4j
public class DeleteBrandRunService {

    private static final String MODULE_TYPE = "DELETE_BRAND";
    private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

    private final FileTaskMapper fileTaskMapper;
    private final FileResultMapper fileResultMapper;
    private final LocalFileStorageService localFileStorageService;
    private final DeleteBrandTaskCacheService deleteBrandTaskCacheService;
    private final ZiniaoShopSwitchService ziniaoShopSwitchService;
    private final OssStorageService ossStorageService;
    private final ObjectMapper objectMapper;
    private final DeleteBrandProgressProperties deleteBrandProgressProperties;

    public DeleteBrandRunVo run(DeleteBrandRunRequest request) {
        if (request.getUserId() == null || request.getUserId() <= 0) {
            throw new BusinessException("user_id 不合法");
        }

        FileTaskEntity task = new FileTaskEntity();
        task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
        task.setModuleType(MODULE_TYPE);
        task.setTaskMode("IMMEDIATE");
        task.setStatus("RUNNING");
        task.setSourceFileCount(request.getFiles().size());
        task.setCreatedBy("user:" + request.getUserId());
        task.setUserId(request.getUserId());
        task.setRequestJson(JSONUtil.toJsonStr(request));
        task.setCreatedAt(LocalDateTime.now());
        task.setUpdatedAt(LocalDateTime.now());
        fileTaskMapper.insert(task);
        deleteBrandTaskCacheService.saveTaskCache(task);

        List<DeleteBrandResultItemVo> items = new ArrayList<>();
        int parsedSuccessCount = 0;
        int parsedFailedCount = 0;
        int processableFileCount = 0;
        Map<String, DeleteBrandParsedFileCacheDto> parsedPayloadByFileIdentity = new LinkedHashMap<>();
        Map<String, ZiniaoShopMatchResultVo> storeMatchByShopName = new LinkedHashMap<>();

        for (DeleteBrandSourceFileDto sourceFile : request.getFiles()) {
            DeleteBrandResultItemVo item = new DeleteBrandResultItemVo();
            item.setTaskId(task.getId());
            item.setFileKey(sourceFile.getFileKey());
            item.setSourceFilename(sourceFile.getOriginalFilename());
            item.setShopName(resolveShopName(sourceFile));
            try {
                File inputFile = localFileStorageService.findLocalSourceFile(sourceFile.getFileKey());
                if (inputFile == null || !inputFile.exists()) {
                    throw new BusinessException("上传文件不存在，请重新上传");
                }
                ParsedDeleteBrandFile parsed = parseDeleteBrandFile(inputFile);
                item.setCountryCount(parsed.countries().size());
                item.setCountries(parsed.countries());
                item.setTotalRows(parsed.totalRows());
                item.setTruncated(false);
                item.setPreviewRows(parsed.previewRows());

                try {
                    String normalizedShopName = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
                    ZiniaoShopMatchResultVo matchResult = normalizedShopName.isBlank()
                            ? ziniaoShopSwitchService.emptyMatchResult()
                            : storeMatchByShopName.computeIfAbsent(normalizedShopName,
                            ignored -> ziniaoShopSwitchService.findIndexedStoreByName(item.getShopName()));
                    item.setMatchStatus(matchResult.getMatchStatus());
                    item.setMatchMessage(matchResult.getMatchMessage());
                    item.setMatched(matchResult.isMatched());
                    if (matchResult.isMatched()) {
                        item.setShopId(matchResult.getShopId());
                        item.setPlatform(matchResult.getPlatform());
                        item.setOpenStoreUrl(matchResult.getOpenStoreUrl());
                    }
                } catch (BusinessException ex) {
                    String message = ex.getMessage();
                    if (ziniaoShopSwitchService.isSkippableMatchError(ex)) {
                        log.warn("[ziniao-match] suppressed upstream permission error for {}, msg: {}", item.getShopName(), message);
                        item.setMatched(false);
                    } else {
                        throw ex;
                    }
                }

                if (item.isMatched()) {
                    String fileIdentity = resolveFileIdentity(sourceFile);
                    DeleteBrandParsedFileCacheDto cacheDto = new DeleteBrandParsedFileCacheDto();
                    cacheDto.setFileKey(sourceFile.getFileKey());
                    cacheDto.setSourceFilename(sourceFile.getOriginalFilename());
                    cacheDto.setShopName(item.getShopName());
                    cacheDto.setTotalRows(parsed.totalRows());
                    cacheDto.setCountries(parsed.countries());
                    cacheDto.setPreviewRows(parsed.previewRows());
                    parsedPayloadByFileIdentity.put(fileIdentity, cacheDto);
                    processableFileCount++;
                    parsedSuccessCount++;
                } else {
                    parsedFailedCount++;
                }

                item.setSuccess(true);

                FileResultEntity resultEntity = new FileResultEntity();
                resultEntity.setTaskId(task.getId());
                resultEntity.setModuleType(MODULE_TYPE);
                resultEntity.setSourceFilename(item.getSourceFilename());
                resultEntity.setSourceFileUrl(sourceFile.getFileKey());
                resultEntity.setResultFilename(null);
                resultEntity.setResultFileUrl(null);
                resultEntity.setResultFileSize(0L);
                resultEntity.setResultContentType(CONTENT_TYPE_XLSX);
                resultEntity.setRowCount(parsed.totalRows());
                resultEntity.setSuccess(0);
                resultEntity.setUserId(request.getUserId());
                resultEntity.setCreatedAt(LocalDateTime.now());
                fileResultMapper.insert(resultEntity);
                item.setResultId(resultEntity.getId());
            } catch (Exception ex) {
                item.setSuccess(false);
                item.setError(ex.getMessage());
                parsedFailedCount++;

                FileResultEntity resultEntity = new FileResultEntity();
                resultEntity.setTaskId(task.getId());
                resultEntity.setModuleType(MODULE_TYPE);
                resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
                resultEntity.setSourceFileUrl(sourceFile.getFileKey());
                resultEntity.setSuccess(0);
                resultEntity.setErrorMessage(ex.getMessage());
                resultEntity.setUserId(request.getUserId());
                resultEntity.setCreatedAt(LocalDateTime.now());
                fileResultMapper.insert(resultEntity);
                item.setResultId(resultEntity.getId());
            }
            items.add(item);
        }

        task.setSuccessFileCount(parsedSuccessCount);
        task.setFailedFileCount(parsedFailedCount);
        task.setResultJson(JSONUtil.toJsonStr(items));
        task.setUpdatedAt(LocalDateTime.now());
        task.setSourceFileCount(processableFileCount);
        if (processableFileCount <= 0) {
            task.setStatus("FAILED");
            task.setErrorMessage(parsedFailedCount > 0 ? "全部文件解析或匹配失败" : null);
            task.setFinishedAt(LocalDateTime.now());
        }
        fileTaskMapper.updateById(task);
        if ("RUNNING".equals(task.getStatus())) {
            deleteBrandTaskCacheService.saveTaskCache(task);
        }

        if (!parsedPayloadByFileIdentity.isEmpty()) {
            deleteBrandTaskCacheService.saveParsedPayload(task.getId(), parsedPayloadByFileIdentity);
        }

        DeleteBrandRunVo vo = new DeleteBrandRunVo();
        vo.setTotal(request.getFiles().size());
        vo.setSuccessCount(parsedSuccessCount);
        vo.setFailedCount(parsedFailedCount);
        vo.setItems(items.stream().peek(item -> item.setTaskStatus("RUNNING")).toList());
        return vo;
    }

    public DeleteBrandHistoryVo listHistory(Long userId) {
        DeleteBrandHistoryVo vo = new DeleteBrandHistoryVo();
        List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                .eq(FileResultEntity::getUserId, userId)
                .orderByDesc(FileResultEntity::getCreatedAt)
                .last("limit 100"));

        // 补充信息
        Map<Long, List<DeleteBrandResultItemVo>> itemsByTaskId = new LinkedHashMap<>();
        Map<Long, String> statusByTaskId = new LinkedHashMap<>();
        List<Long> taskIds = entities.stream()
                .map(FileResultEntity::getTaskId)
                .filter(id -> id != null && id > 0)
                .distinct()
                .toList();
        if (!taskIds.isEmpty()) {
            List<FileTaskEntity> tasks = fileTaskMapper.selectBatchIds(taskIds);
            for (FileTaskEntity task : tasks) {
                try {
                    statusByTaskId.put(task.getId(), task.getStatus());
                    if (task.getResultJson() != null && !task.getResultJson().isBlank()) {
                        List<DeleteBrandResultItemVo> taskItems = objectMapper.readValue(task.getResultJson(), new TypeReference<List<DeleteBrandResultItemVo>>() {
                        });
                        itemsByTaskId.put(task.getId(), taskItems);
                    }
                } catch (Exception ignored) {
                }
            }
        }

        vo.setItems(entities.stream().map(entity -> {
            DeleteBrandResultItemVo item = new DeleteBrandResultItemVo();
            item.setResultId(entity.getId());
            item.setTaskId(entity.getTaskId());
            item.setFileKey(entity.getSourceFileUrl());
            item.setSourceFilename(entity.getSourceFilename());
            item.setShopName(entity.getSourceFilename() == null ? null : FileUtil.mainName(entity.getSourceFilename()));
            item.setOutputFilename(entity.getResultFilename());
            item.setDownloadUrl(entity.getResultFileUrl() == null ? null : ossStorageService.getPublicUrl(entity.getResultFileUrl()));
            item.setTotalRows(entity.getRowCount());
            item.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
            item.setError(entity.getErrorMessage());

            // 以 task.resultJson 中的项为准补全 matched/shopId/platform/openStoreUrl/错误信息
            // 真实补充
            if (entity.getTaskId() != null) {
                item.setTaskStatus(statusByTaskId.get(entity.getTaskId()));
                List<DeleteBrandResultItemVo> taskItems = itemsByTaskId.get(entity.getTaskId());
                if (taskItems != null && entity.getSourceFilename() != null && !entity.getSourceFilename().isBlank()) {
                    for (DeleteBrandResultItemVo candidate : taskItems) {
                        if (candidate == null) {
                            continue;
                        }
                        if (entity.getSourceFilename().equals(candidate.getSourceFilename())) {
                            item.setMatched(candidate.isMatched());
                            item.setMatchStatus(candidate.getMatchStatus());
                            item.setMatchMessage(candidate.getMatchMessage());
                            // 如果物理结果已经是成功了，说明已经处理过了，强制置为匹配成功
                            if (item.isSuccess()) {
                                item.setMatched(true);
                                item.setMatchStatus("MATCHED");
                                item.setMatchMessage(null);
                            }
                            
                            item.setShopId(candidate.getShopId());
                            item.setPlatform(candidate.getPlatform());
                            item.setOpenStoreUrl(candidate.getOpenStoreUrl());
                            
                            // 历史表可能只有通用 errorMessage；优先返回任务当时的真实 error
                            // 但是，如果物理结果是成功的，就不应该显示所谓的“未匹配”报错
                            if (item.isSuccess()) {
                                item.setError(null);
                            } else if ((item.getError() == null || item.getError().isBlank()) && candidate.getError() != null && !candidate.getError().isBlank()) {
                                item.setError(candidate.getError());
                            }
                            break;
                        }
                    }
                }
            }
            return item;
        }).toList());

        return vo;
    }

    public DeleteBrandTaskDeletionStatusVo getTaskDeletionStatus(Long taskId, Long userId) {
        FileTaskEntity task = fileTaskMapper.selectById(taskId);
        if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
            throw new BusinessException("任务不存在");
        }

        Long remainingResultCount = fileResultMapper.selectCount(new LambdaQueryWrapper<FileResultEntity>()
                .eq(FileResultEntity::getTaskId, taskId)
                .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                .eq(FileResultEntity::getUserId, userId));

        DeleteBrandTaskDeletionStatusVo vo = new DeleteBrandTaskDeletionStatusVo();
        vo.setTaskId(taskId);
        vo.setRemainingResultCount(remainingResultCount == null ? 0L : remainingResultCount);
        vo.setDeleted(vo.getRemainingResultCount() == 0);
        return vo;
    }

    public void deleteHistory(Long resultId, Long userId) {
        FileResultEntity entity = fileResultMapper.selectById(resultId);
        if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
            throw new BusinessException("记录不存在");
        }
        fileResultMapper.deleteById(resultId);
    }

    private ParsedDeleteBrandFile parseDeleteBrandFile(File inputFile) {
        DataFormatter formatter = new DataFormatter();
        try (FileInputStream fis = new FileInputStream(inputFile); Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis)) {
            Sheet sheet = workbook.getSheetAt(0);
            Row titleRow = sheet.getRow(0);
            Row headerRow = sheet.getRow(1);
            if (titleRow == null || headerRow == null) {
                throw new BusinessException("Excel 表头不完整");
            }

            List<CountryColumnPair> pairs = resolveCountryPairs(titleRow, headerRow, formatter);
            if (pairs.isEmpty()) {
                throw new BusinessException("未识别到删除品牌表头");
            }

            Map<String, Map<String, DeleteBrandCountryAsinVo>> grouped = new LinkedHashMap<>();
            Map<String, String> displayCountryNames = new LinkedHashMap<>();

            for (int rowNum = 2; rowNum <= sheet.getLastRowNum(); rowNum++) {
                Row row = sheet.getRow(rowNum);
                if (row == null) {
                    continue;
                }
                for (CountryColumnPair pair : pairs) {
                    String country = pair.country();
                    String asin = normalizeCellText(formatter.formatCellValue(row.getCell(pair.asinColumnIndex())));
                    String status = normalizeCellText(formatter.formatCellValue(row.getCell(pair.statusColumnIndex())));
                    if (asin.isBlank() && status.isBlank()) {
                        continue;
                    }

                    String asinKey = normalizeAsinKey(asin);
                    if (asinKey.isBlank()) {
                        continue;
                    }

                    displayCountryNames.putIfAbsent(country, country);
                    Map<String, DeleteBrandCountryAsinVo> countryMap = grouped.computeIfAbsent(country, ignored -> new LinkedHashMap<>());
                    if (countryMap.containsKey(asinKey)) {
                        continue;
                    }

                    DeleteBrandCountryAsinVo item = new DeleteBrandCountryAsinVo();
                    item.setRowIndex(rowNum + 1);
                    item.setAsin(asin);
                    item.setStatus(status);
                    countryMap.put(asinKey, item);
                }
            }

            List<DeleteBrandCountryGroupVo> countries = new ArrayList<>();
            List<DeleteBrandPreviewRowVo> previewRows = new ArrayList<>();
            int totalRows = 0;
            for (CountryColumnPair pair : pairs) {
                String country = pair.country();
                Map<String, DeleteBrandCountryAsinVo> countryMap = grouped.get(country);
                if (countryMap == null || countryMap.isEmpty()) {
                    continue;
                }
                DeleteBrandCountryGroupVo groupVo = new DeleteBrandCountryGroupVo();
                groupVo.setCountry(displayCountryNames.getOrDefault(country, country));
                groupVo.getItems().addAll(countryMap.values());
                groupVo.setAsinCount(groupVo.getItems().size());
                countries.add(groupVo);

                for (DeleteBrandCountryAsinVo asinVo : groupVo.getItems()) {
                    DeleteBrandPreviewRowVo preview = new DeleteBrandPreviewRowVo();
                    preview.setRowIndex(asinVo.getRowIndex());
                    preview.setCountry(groupVo.getCountry());
                    preview.setAsin(asinVo.getAsin());
                    preview.setStatus(asinVo.getStatus());
                    previewRows.add(preview);
                    totalRows++;
                }
            }

            return new ParsedDeleteBrandFile(totalRows, countries, previewRows);
        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new BusinessException("读取删除品牌 Excel 失败");
        }
    }

    private String normalizeAsinKey(String value) {
        if (value == null) {
            return "";
        }
        return value.replace(" ", "").trim().toUpperCase(Locale.ROOT);
    }

    private List<CountryColumnPair> resolveCountryPairs(Row titleRow, Row headerRow, DataFormatter formatter) {
        List<CountryColumnPair> pairs = new ArrayList<>();
        int lastCellNum = Math.max(titleRow.getLastCellNum(), headerRow.getLastCellNum());
        for (int i = 0; i < lastCellNum; i++) {
            String title = normalizeCellText(formatter.formatCellValue(titleRow.getCell(i)));
            String firstHeader = normalizeCellText(formatter.formatCellValue(headerRow.getCell(i)));
            String secondHeader = normalizeCellText(formatter.formatCellValue(headerRow.getCell(i + 1)));
            if (title.isBlank()) {
                continue;
            }
            if ("删除ASIN".equals(firstHeader) && "状态".equals(secondHeader)) {
                pairs.add(new CountryColumnPair(title, i, i + 1));
                i++;
            }
        }
        return pairs;
    }

    private String resolveShopName(DeleteBrandSourceFileDto sourceFile) {
        String name = sourceFile.getOriginalFilename();
        if (name == null || name.isBlank()) {
            return "";
        }
        return FileUtil.mainName(name).trim();
    }

    private String normalizeCellText(String value) {
        if (value == null) {
            return "";
        }
        return value.replace("\ufeff", "")
                .replace("\u3000", " ")
                .replace("\r\n", " ")
                .replace("\r", " ")
                .replace("\n", " ")
                .replace("\t", " ")
                .trim()
                .replaceAll("\\s+", " ");
    }

    private record CountryColumnPair(String country, int asinColumnIndex, int statusColumnIndex) {
    }

    private String resolveFileIdentity(DeleteBrandSourceFileDto sourceFile) {
        if (sourceFile != null && sourceFile.getFileKey() != null && !sourceFile.getFileKey().isBlank()) {
            return sourceFile.getFileKey().trim();
        }
        return sourceFile == null ? "" : String.valueOf(sourceFile.getOriginalFilename());
    }

    public DeleteBrandTaskDetailVo getTaskDetail(Long taskId) {
        FileTaskEntity task = fileTaskMapper.selectById(taskId);
        if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
            throw new BusinessException("任务不存在");
        }
        return buildTaskDetail(task);
    }

    public com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo getTaskDetails(java.util.List<Long> taskIds) {
        com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo vo = new com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo();
        if (taskIds == null || taskIds.isEmpty()) {
            return vo;
        }

        java.util.List<Long> normalizedTaskIds = taskIds.stream()
                .filter(id -> id != null && id > 0)
                .distinct()
                .limit(50)
                .toList();
        if (normalizedTaskIds.isEmpty()) {
            return vo;
        }

        java.util.Map<Long, FileTaskEntity> cachedTasks = deleteBrandTaskCacheService.getTaskCacheBatch(normalizedTaskIds);
        java.util.List<Long> missingTaskIds = new java.util.ArrayList<>();
        for (Long taskId : normalizedTaskIds) {
            if (!cachedTasks.containsKey(taskId)) {
                missingTaskIds.add(taskId);
            }
        }

        java.util.Map<Long, FileTaskEntity> taskById = new java.util.LinkedHashMap<>(cachedTasks);
        if (!missingTaskIds.isEmpty()) {
            java.util.List<FileTaskEntity> dbTasks = fileTaskMapper.selectBatchIds(missingTaskIds).stream()
                    .filter(task -> task != null && MODULE_TYPE.equals(task.getModuleType()))
                    .toList();
            for (FileTaskEntity dbTask : dbTasks) {
                taskById.put(dbTask.getId(), dbTask);
                // Save back to cache
                if ("RUNNING".equals(dbTask.getStatus())) {
                    deleteBrandTaskCacheService.saveTaskCache(dbTask);
                }
            }
        }

        java.util.Map<Long, java.util.Map<Object, Object>> progressByTaskId = deleteBrandTaskCacheService.getProgressBatch(normalizedTaskIds);

        for (Long taskId : normalizedTaskIds) {
            FileTaskEntity task = taskById.get(taskId);
            if (task == null) {
                vo.getMissingTaskIds().add(taskId);
                continue;
            }
            vo.getItems().add(buildTaskDetail(task, progressByTaskId.get(taskId)));
        }
        return vo;
    }

    private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task) {
        return buildTaskDetail(task, null);
    }

    private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task, java.util.Map<Object, Object> progress) {
        task = refreshIndexedMatchesIfNeeded(task);
        DeleteBrandTaskItemVo taskVo = new DeleteBrandTaskItemVo();
        taskVo.setId(task.getId());
        taskVo.setTaskNo(task.getTaskNo());
        taskVo.setStatus(task.getStatus());
        taskVo.setSourceFileCount(task.getSourceFileCount());
        taskVo.setSuccessFileCount(task.getSuccessFileCount());
        taskVo.setFailedFileCount(task.getFailedFileCount());
        taskVo.setErrorMessage(task.getErrorMessage());
        taskVo.setCreatedAt(task.getCreatedAt());
        taskVo.setUpdatedAt(task.getUpdatedAt());
        taskVo.setFinishedAt(task.getFinishedAt());
        // 轮询期避免每次都查 biz_file_result（会放大 DB 压力）；仅任务成功后再提供下载信息
        if ("SUCCESS".equals(task.getStatus())) {
            taskVo.setDownloadUrl(resolveTaskDownloadUrl(task.getId()));
            taskVo.setDownloadFilename(resolveTaskDownloadFilename(task.getId()));
        } else {
            taskVo.setDownloadUrl(null);
            taskVo.setDownloadFilename(null);
        }

        DeleteBrandLineProgressVo progressVo = buildLineProgress(task.getId(), progress);

        DeleteBrandTaskDetailVo detail = new DeleteBrandTaskDetailVo();
        detail.setTask(taskVo);
        detail.setLine_progress(progressVo);
        detail.setItems(resolveTaskResultItems(task));
        return detail;
    }

    private List<DeleteBrandResultItemVo> resolveTaskResultItems(FileTaskEntity task) {
        if (task == null || task.getResultJson() == null || task.getResultJson().isBlank()) {
            return List.of();
        }
        try {
            List<DeleteBrandResultItemVo> items = objectMapper.readValue(task.getResultJson(), new TypeReference<List<DeleteBrandResultItemVo>>() {
            });
            if (items == null || items.isEmpty()) {
                return List.of();
            }
            return items.stream().peek(item -> {
                if (item == null) {
                    return;
                }
                item.setTaskId(task.getId());
                item.setTaskStatus(task.getStatus());
                if ("SUCCESS".equals(task.getStatus()) && item.isSuccess()) {
                    item.setMatched(true);
                    item.setMatchStatus(ZiniaoShopIndexService.MATCH_STATUS_MATCHED);
                    item.setMatchMessage(null);
                }
            }).toList();
        } catch (Exception ignored) {
            return List.of();
        }
    }

    private FileTaskEntity refreshIndexedMatchesIfNeeded(FileTaskEntity task) {
        if (task == null || task.getResultJson() == null || task.getResultJson().isBlank()) {
            return task;
        }
        if (!"RUNNING".equals(task.getStatus()) && !"FAILED".equals(task.getStatus())) {
            return task;
        }
        try {
            List<DeleteBrandResultItemVo> items = objectMapper.readValue(task.getResultJson(), new TypeReference<List<DeleteBrandResultItemVo>>() {
            });
            if (items == null || items.isEmpty()) {
                return task;
            }
            boolean changed = false;
            int matchedCount = 0;
            int unmatchedCount = 0;
            for (DeleteBrandResultItemVo item : items) {
                if (item == null) {
                    continue;
                }
                String currentMatchStatus = item.getMatchStatus();
                if (currentMatchStatus == null || ZiniaoShopIndexService.MATCH_STATUS_MATCHED.equals(currentMatchStatus)) {
                    if (item.isMatched()) {
                        matchedCount++;
                    } else {
                        unmatchedCount++;
                    }
                    continue;
                }
                if (!ZiniaoShopIndexService.MATCH_STATUS_PENDING.equals(currentMatchStatus)
                        && !ZiniaoShopIndexService.MATCH_STATUS_STALE.equals(currentMatchStatus)) {
                    unmatchedCount++;
                    continue;
                }
                String normalizedShopName = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
                if (normalizedShopName.isBlank()) {
                    unmatchedCount++;
                    continue;
                }
                ZiniaoShopMatchResultVo refreshed = ziniaoShopSwitchService.findIndexedStoreByName(item.getShopName());
                if (refreshed != null) {
                    String oldStatus = item.getMatchStatus();
                    boolean oldMatched = item.isMatched();
                    String oldShopId = item.getShopId();
                    String oldPlatform = item.getPlatform();
                    String oldOpenStoreUrl = item.getOpenStoreUrl();
                    String oldMessage = item.getMatchMessage();

                    item.setMatched(refreshed.isMatched());
                    item.setMatchStatus(refreshed.getMatchStatus());
                    item.setMatchMessage(refreshed.getMatchMessage());
                    item.setShopId(refreshed.getShopId());
                    item.setPlatform(refreshed.getPlatform());
                    item.setOpenStoreUrl(refreshed.getOpenStoreUrl());

                    if (!java.util.Objects.equals(oldStatus, item.getMatchStatus())
                            || oldMatched != item.isMatched()
                            || !java.util.Objects.equals(oldShopId, item.getShopId())
                            || !java.util.Objects.equals(oldPlatform, item.getPlatform())
                            || !java.util.Objects.equals(oldOpenStoreUrl, item.getOpenStoreUrl())
                            || !java.util.Objects.equals(oldMessage, item.getMatchMessage())) {
                        changed = true;
                    }
                }
                if (item.isMatched()) {
                    matchedCount++;
                } else {
                    unmatchedCount++;
                }
            }
            if (!changed) {
                return task;
            }
            task.setResultJson(JSONUtil.toJsonStr(items));
            task.setSourceFileCount(matchedCount);
            task.setFailedFileCount(unmatchedCount);
            task.setUpdatedAt(LocalDateTime.now());
            if (matchedCount > 0 && "FAILED".equals(task.getStatus())) {
                task.setStatus("RUNNING");
                task.setErrorMessage(null);
                task.setFinishedAt(null);
            }
            fileTaskMapper.updateById(task);
            if ("RUNNING".equals(task.getStatus())) {
                deleteBrandTaskCacheService.saveTaskCache(task);
            }
        } catch (Exception ex) {
            log.debug("refreshIndexedMatchesIfNeeded skipped for taskId={}, msg={}", task.getId(), ex.getMessage());
        }
        return task;
    }

    private DeleteBrandLineProgressVo buildLineProgress(Long taskId) {
        return buildLineProgress(taskId, null);
    }

    private DeleteBrandLineProgressVo buildLineProgress(Long taskId, Map<Object, Object> progress) {
        Map<Object, Object> resolvedProgress = progress != null ? progress : deleteBrandTaskCacheService.getProgress(taskId);
        DeleteBrandLineProgressVo vo = new DeleteBrandLineProgressVo();
        if (resolvedProgress == null || resolvedProgress.isEmpty()) {
            vo.setHas_progress(false);
            vo.setInfo(null);
            return vo;
        }
        DeleteBrandLineProgressInfoVo info = new DeleteBrandLineProgressInfoVo();
        info.setFile_index(parseInt(resolvedProgress.get("file_index")));
        info.setFile_total(parseInt(resolvedProgress.get("file_total")));
        info.setFile_name(stringValue(resolvedProgress.get("file_name")));
        info.setCurrent_line(parseInt(resolvedProgress.get("current_line")));
        info.setTotal_lines(parseInt(resolvedProgress.get("total_lines")));
        info.setCurrent_country(stringValue(resolvedProgress.get("current_country")));
        info.setCurrent_asin(stringValue(resolvedProgress.get("current_asin")));
        info.setFinished_files(parseInt(resolvedProgress.get("finished_files")));
        info.setPhase(stringValue(resolvedProgress.get("phase")));
        vo.setHas_progress(true);
        vo.setInfo(info);
        return vo;
    }

    private Integer parseInt(Object value) {
        if (value == null) {
            return null;
        }
        try {
            return Integer.parseInt(String.valueOf(value));
        } catch (Exception ex) {
            return null;
        }
    }

    private String stringValue(Object value) {
        if (value == null) {
            return null;
        }
        String raw = String.valueOf(value);
        return raw.isBlank() ? null : raw;
    }

    private String resolveFileIdentity(DeleteBrandResultFileDto fileDto) {
        if (fileDto != null && fileDto.getFileKey() != null && !fileDto.getFileKey().isBlank()) {
            return fileDto.getFileKey().trim();
        }
        return fileDto == null ? "" : blankToEmpty(fileDto.getSourceFilename());
    }

    private void validateChunk(DeleteBrandResultFileDto fileDto, DeleteBrandParsedFileCacheDto parsedFile) {
        if (fileDto.getChunkIndex() == null || fileDto.getChunkTotal() == null
                || fileDto.getChunkIndex() <= 0 || fileDto.getChunkTotal() <= 0
                || fileDto.getChunkIndex() > fileDto.getChunkTotal()) {
            throw new BusinessException("分片参数不合法");
        }
        if (fileDto.getTotalRows() != null && parsedFile.getTotalRows() != null && fileDto.getTotalRows() > parsedFile.getTotalRows()) {
            throw new BusinessException("totalRows 不合法");
        }
    }

    private boolean isFileCompleted(DeleteBrandResultFileDto fileDto) {
        return fileDto.getChunkIndex() != null && fileDto.getChunkTotal() != null && fileDto.getChunkIndex().equals(fileDto.getChunkTotal());
    }

    private int defaultInt(Integer value, int defaultValue) {
        return value == null ? defaultValue : value;
    }

    private String blankToEmpty(String value) {
        return value == null ? "" : value.trim();
    }

    private void validateResultFileAgainstParsed(DeleteBrandResultFileDto fileDto, DeleteBrandParsedFileCacheDto parsedFile) {
        Map<String, DeleteBrandCountryGroupVo> byCountry = new LinkedHashMap<>();
        for (DeleteBrandCountryGroupVo group : parsedFile.getCountries()) {
            byCountry.put(group.getCountry(), group);
        }

        for (DeleteBrandProcessedCountryDto countryDto : fileDto.getCountries()) {
            DeleteBrandCountryGroupVo group = byCountry.get(countryDto.getCountry());
            if (group == null) {
                throw new BusinessException("country 不匹配: " + fileDto.getSourceFilename() + " / " + countryDto.getCountry());
            }

            Map<String, DeleteBrandCountryAsinVo> allowed = new LinkedHashMap<>();
            for (DeleteBrandCountryAsinVo asinVo : group.getItems()) {
                allowed.put(normalizeAsinKey(asinVo.getAsin()), asinVo);
            }

            for (DeleteBrandCountryResultItemDto itemDto : countryDto.getItems()) {
                String key = normalizeAsinKey(itemDto.getAsin());
                if (key.isBlank() || !allowed.containsKey(key)) {
                    throw new BusinessException("asin 不匹配: " + fileDto.getSourceFilename() + " / " + countryDto.getCountry() + " / " + itemDto.getAsin());
                }
            }
        }
    }

    @Transactional
    public void submitResult(Long taskId, DeleteBrandSubmitResultRequest request) {
        log.info("[DeleteBrand] Received submitResult for taskId: {}, files count: {}", taskId, request == null || request.getFiles() == null ? 0 : request.getFiles().size());

        if (taskId == null || taskId <= 0) {
            throw new BusinessException("taskId 不合法");
        }
        if (request == null || request.getFiles() == null || request.getFiles().isEmpty()) {
            throw new BusinessException("files 不能为空");
        }

        FileTaskEntity task = fileTaskMapper.selectById(taskId);
        if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
            throw new BusinessException("任务不存在");
        }
        if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
            throw new BusinessException("任务已结束，拒绝继续提交结果");
        }

        Map<String, DeleteBrandParsedFileCacheDto> parsedPayload = deleteBrandTaskCacheService.getParsedPayload(taskId,
                new TypeReference<Map<String, DeleteBrandParsedFileCacheDto>>() {
                });
        if (parsedPayload == null || parsedPayload.isEmpty()) {
            throw new BusinessException("任务原始数据已过期，请重新执行解析");
        }

        if (!"RUNNING".equals(task.getStatus())) {
            task.setStatus("RUNNING");
            task.setErrorMessage(null);
            task.setUpdatedAt(LocalDateTime.now());
            task.setFinishedAt(null);
            fileTaskMapper.updateById(task);
        }


        for (DeleteBrandResultFileDto fileDto : request.getFiles()) {
            log.info("[DeleteBrand] submitResult processing chunk -> taskId: {}, file: {}, chunkIndex: {}/{}, processedRows: {}/{}",
                    taskId, fileDto.getSourceFilename(), fileDto.getChunkIndex(), fileDto.getChunkTotal(),
                    fileDto.getProcessedRows(), fileDto.getTotalRows());

            String fileIdentity = resolveFileIdentity(fileDto);
            if (fileIdentity.isBlank()) {
                throw new BusinessException("fileKey/sourceFilename 不能为空");
            }
            DeleteBrandParsedFileCacheDto parsedFile = parsedPayload.get(fileIdentity);
            if (parsedFile == null) {
                log.warn("[DeleteBrand] parsed payload not found for identity: {}", fileIdentity);
                throw new BusinessException("文件标识不匹配: " + fileIdentity);
            }

            // 更早的 duplicate fast-path：尽量在较重的校验/进度写入之前直接短路
            if (deleteBrandTaskCacheService.hasSameResultChunk(taskId, fileIdentity, fileDto.getChunkIndex(), fileDto)) {
                log.info("[DeleteBrand] Duplicate chunk ignored for taskId: {}, file: {}, chunkIndex: {}", taskId, fileIdentity, fileDto.getChunkIndex());
                continue;
            }

            validateChunk(fileDto, parsedFile);
            validateResultFileAgainstParsed(fileDto, parsedFile);
            boolean stored = deleteBrandTaskCacheService.mergeResultChunk(taskId, fileIdentity, fileDto.getChunkIndex(), fileDto);
            if (!stored) {
                continue;
            }

            Map<String, String> progress = new LinkedHashMap<>();
            progress.put("phase", DeleteBrandTaskCacheService.PHASE_CRAWLING);
            progress.put("file_index", String.valueOf(defaultInt(fileDto.getFileIndex(), 0)));
            progress.put("file_total", String.valueOf(defaultInt(fileDto.getFileTotal(), parsedPayload.size())));
            progress.put("file_name", blankToEmpty(fileDto.getSourceFilename()));
            progress.put("current_line", String.valueOf(defaultInt(fileDto.getProcessedRows(), 0)));
            progress.put("total_lines", String.valueOf(defaultInt(fileDto.getTotalRows(), parsedFile.getTotalRows() == null ? 0 : parsedFile.getTotalRows())));
            progress.put("current_country", blankToEmpty(fileDto.getCurrentCountry()));
            progress.put("current_asin", blankToEmpty(fileDto.getCurrentAsin()));
            progress.put("updated_at", String.valueOf(System.currentTimeMillis()));
            progress.put("last_heartbeat_at", String.valueOf(System.currentTimeMillis()));
            deleteBrandTaskCacheService.saveProgress(taskId, progress);

            // 如果该文件已完成，立即更新成品计数的 Redis 缓存「提示」，让后续 tryFinalizeTask 能更精确
            if (isFileCompleted(fileDto)) {
                log.info("[DeleteBrand] File chunk transmission fully reached via chunkTotal for taskId: {}, file: {}", taskId, fileDto.getSourceFilename());
                java.util.Map<Object, Object> currentProgress = deleteBrandTaskCacheService.getProgress(taskId);
                int finishedCount = parseInt(currentProgress.get("finished_files")) == null ? 0 : parseInt(currentProgress.get("finished_files"));
                // 这里我们暂不直接 increment，而是标记需要重算或在 tryFinalizeTask 中重新计算
                // 为了保险，我们直接在 tryFinalizeTask 里重新遍历分片状态
            }

        }

        tryFinalizeTask(taskId, false);
    }

    public void tryFinalizeTask(Long taskId, boolean fromCompensation) {
        if (taskId == null || taskId <= 0) {
            return;
        }

        FileTaskEntity task = fileTaskMapper.selectById(taskId);
        if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
            return;
        }
        if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
            return;
        }

        Map<String, DeleteBrandParsedFileCacheDto> parsedPayload = deleteBrandTaskCacheService.getParsedPayload(taskId,
                new TypeReference<Map<String, DeleteBrandParsedFileCacheDto>>() {
                });
        if (parsedPayload == null || parsedPayload.isEmpty()) {
            return;
        }

        int expectedFiles = parsedPayload.size();
        // 移除 finishedFilesHint 强行返回逻辑，因为 Redis 里的 finished_files 可能是旧的，
        // 既然进入了 tryFinalizeTask，就说明有新片到达，应该以 loadMergedChunks 为准


        Map<String, List<DeleteBrandResultFileDto>> mergedByFile = loadMergedChunks(taskId);
        int finishedFiles = countCompletedFiles(mergedByFile);

        Map<String, String> progress = new LinkedHashMap<>();
        progress.put("finished_files", String.valueOf(finishedFiles));
        progress.put("updated_at", String.valueOf(System.currentTimeMillis()));
        if (fromCompensation) {
            progress.put("last_heartbeat_at", String.valueOf(System.currentTimeMillis()));
        }
        deleteBrandTaskCacheService.saveProgress(taskId, progress);

        task.setSuccessFileCount(finishedFiles);
        task.setUpdatedAt(LocalDateTime.now());
        fileTaskMapper.updateById(task);

        if (finishedFiles < expectedFiles) {
            log.info("[DeleteBrand] tryFinalizeTask skipping -> finishedFiles ({}) < expectedFiles ({})", finishedFiles, expectedFiles);
            return;
        }
        
        log.info("[DeleteBrand] tryFinalizeTask calling finalizeTask -> finishedFiles: {}, expectedFiles: {}", finishedFiles, expectedFiles);

        finalizeTask(task, parsedPayload, mergedByFile);
    }

    private Map<String, List<DeleteBrandResultFileDto>> loadMergedChunks(Long taskId) {
        Map<String, List<String>> groupedJson = deleteBrandTaskCacheService.groupResultChunkJsonByFile(taskId);
        Map<String, List<DeleteBrandResultFileDto>> merged = new LinkedHashMap<>();
        for (Map.Entry<String, List<String>> entry : groupedJson.entrySet()) {
            List<DeleteBrandResultFileDto> chunks = new ArrayList<>();
            for (String raw : entry.getValue()) {
                try {
                    chunks.add(objectMapper.readValue(raw, DeleteBrandResultFileDto.class));
                } catch (Exception ex) {
                    throw new BusinessException("读取删除品牌结果分片失败");
                }
            }
            chunks.sort(Comparator.comparing(DeleteBrandResultFileDto::getChunkIndex, Comparator.nullsLast(Integer::compareTo)));
            merged.put(entry.getKey(), chunks);
        }
        return merged;
    }

    private int countCompletedFiles(Map<String, List<DeleteBrandResultFileDto>> mergedByFile) {
        int finishedFiles = 0;
        for (List<DeleteBrandResultFileDto> chunks : mergedByFile.values()) {
            if (isMergedFileCompleted(chunks)) {
                finishedFiles++;
            }
        }
        return finishedFiles;
    }

    private boolean isMergedFileCompleted(List<DeleteBrandResultFileDto> chunks) {
        if (chunks == null || chunks.isEmpty()) {
            return false;
        }
        Integer chunkTotal = chunks.get(0).getChunkTotal();
        if (chunkTotal == null || chunkTotal <= 0 || chunks.size() < chunkTotal) {
            return false;
        }
        Set<Integer> indexes = new LinkedHashSet<>();
        for (DeleteBrandResultFileDto chunk : chunks) {
            if (chunk.getChunkTotal() == null || !chunkTotal.equals(chunk.getChunkTotal()) || chunk.getChunkIndex() == null) {
                return false;
            }
            indexes.add(chunk.getChunkIndex());
        }
        for (int i = 1; i <= chunkTotal; i++) {
            if (!indexes.contains(i)) {
                return false;
            }
        }
        return true;
    }

    private void finalizeTask(FileTaskEntity task,
                              Map<String, DeleteBrandParsedFileCacheDto> parsedPayload,
                              Map<String, List<DeleteBrandResultFileDto>> mergedByFile) {
        try {
            deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
                    "phase", DeleteBrandTaskCacheService.PHASE_ASSEMBLING,
                    "finished_files", String.valueOf(parsedPayload.size()),
                    "updated_at", String.valueOf(System.currentTimeMillis())
            ));

            List<FileResultEntity> resultEntities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                    .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                    .eq(FileResultEntity::getTaskId, task.getId())
                    .orderByAsc(FileResultEntity::getId));
            Map<String, FileResultEntity> resultEntityByIdentity = new LinkedHashMap<>();
            for (FileResultEntity entity : resultEntities) {
                String identity = entity.getSourceFileUrl();
                if (identity == null || identity.isBlank()) {
                    identity = blankToEmpty(entity.getSourceFilename());
                }
                if (!identity.isBlank()) {
                    resultEntityByIdentity.putIfAbsent(identity, entity);
                }
            }

            int successCount = 0;
            List<DeleteBrandResultItemVo> finalItems = new ArrayList<>();
            for (Map.Entry<String, DeleteBrandParsedFileCacheDto> entry : parsedPayload.entrySet()) {
                String fileIdentity = entry.getKey();
                DeleteBrandParsedFileCacheDto parsedFile = entry.getValue();
                List<DeleteBrandResultFileDto> chunks = mergedByFile.get(fileIdentity);
                if (!isMergedFileCompleted(chunks)) {
                    throw new BusinessException("存在未完成的结果分片: " + parsedFile.getSourceFilename());
                }
                MergedDeleteBrandFile mergedFile = mergeChunks(parsedFile, chunks);
                File outputFile = buildResultWorkbookPreserveLayout(task.getId(), parsedFile, mergedFile);

                deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
                        "phase", DeleteBrandTaskCacheService.PHASE_UPLOADING,
                        "file_name", blankToEmpty(parsedFile.getSourceFilename()),
                        "updated_at", String.valueOf(System.currentTimeMillis())
                ));

                String objectKey = ossStorageService.uploadResultFile(outputFile, MODULE_TYPE);
                FileResultEntity resultEntity = resultEntityByIdentity.get(fileIdentity);
                if (resultEntity == null) {
                    throw new BusinessException("结果记录不存在: " + parsedFile.getSourceFilename());
                }
                resultEntity.setResultFilename(outputFile.getName());
                resultEntity.setResultFileUrl(objectKey);
                resultEntity.setResultFileSize(outputFile.length());
                resultEntity.setResultContentType(CONTENT_TYPE_XLSX);
                resultEntity.setRowCount(parsedFile.getTotalRows());
                resultEntity.setSuccess(1);
                resultEntity.setErrorMessage(null);
                fileResultMapper.updateById(resultEntity);

                DeleteBrandResultItemVo item = new DeleteBrandResultItemVo();
                item.setResultId(resultEntity.getId());
                item.setTaskId(task.getId());
                item.setFileKey(parsedFile.getFileKey());
                item.setSourceFilename(parsedFile.getSourceFilename());
                item.setShopName(parsedFile.getShopName());
                item.setOutputFilename(outputFile.getName());
                item.setDownloadUrl(ossStorageService.getPublicUrl(objectKey));
                item.setTotalRows(parsedFile.getTotalRows());
                item.setCountryCount(parsedFile.getCountries() == null ? 0 : parsedFile.getCountries().size());
                item.setCountries(parsedFile.getCountries());
                item.setPreviewRows(parsedFile.getPreviewRows());
                item.setTruncated(false);
                item.setMatched(true);
                item.setMatchStatus("MATCHED");
                item.setMatchMessage(null);
                finalItems.add(item);
                successCount++;
            }

            task.setStatus("SUCCESS");
            task.setSuccessFileCount(successCount);
            task.setErrorMessage(null);
            task.setResultJson(JSONUtil.toJsonStr(finalItems));
            task.setUpdatedAt(LocalDateTime.now());
            task.setFinishedAt(LocalDateTime.now());
            fileTaskMapper.updateById(task);
            deleteBrandTaskCacheService.saveTaskCache(task);

            deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
                    "phase", "success",
                    "file_index", String.valueOf(parsedPayload.size()),
                    "file_total", String.valueOf(parsedPayload.size()),
                    "finished_files", String.valueOf(successCount),
                    "updated_at", String.valueOf(System.currentTimeMillis())
            ));
        } catch (Exception ex) {
            task.setStatus("FAILED");
            task.setErrorMessage(ex.getMessage());
            task.setUpdatedAt(LocalDateTime.now());
            task.setFinishedAt(LocalDateTime.now());
            fileTaskMapper.updateById(task);
            deleteBrandTaskCacheService.saveTaskCache(task);
            deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
                    "phase", DeleteBrandTaskCacheService.PHASE_FAILED,
                    "finished_files", String.valueOf(task.getSuccessFileCount() == null ? 0 : task.getSuccessFileCount()),
                    "updated_at", String.valueOf(System.currentTimeMillis())
            ));
            if (ex instanceof BusinessException businessException) {
                throw businessException;
            }
            throw new BusinessException("删除品牌结果组装失败");
        }
    }

    private MergedDeleteBrandFile mergeChunks(DeleteBrandParsedFileCacheDto parsedFile, List<DeleteBrandResultFileDto> chunks) {
        Integer chunkTotal = chunks.get(0).getChunkTotal();
        for (DeleteBrandResultFileDto chunk : chunks) {
            if (!chunkTotal.equals(chunk.getChunkTotal())) {
                throw new BusinessException("chunkTotal 不一致: " + parsedFile.getSourceFilename());
            }
        }

        Map<String, Map<String, DeleteBrandCountryResultItemDto>> processedByCountry = new LinkedHashMap<>();
        int processedRows = 0;
        for (DeleteBrandResultFileDto chunk : chunks) {
            processedRows = Math.max(processedRows, defaultInt(chunk.getProcessedRows(), 0));
            for (DeleteBrandProcessedCountryDto countryDto : chunk.getCountries()) {
                Map<String, DeleteBrandCountryResultItemDto> byAsin = processedByCountry.computeIfAbsent(countryDto.getCountry(), ignored -> new LinkedHashMap<>());
                for (DeleteBrandCountryResultItemDto itemDto : countryDto.getItems()) {
                    byAsin.put(normalizeAsinKey(itemDto.getAsin()), itemDto);
                }
            }
        }

        List<DeleteBrandProcessedCountryDto> mergedCountries = new ArrayList<>();
        for (DeleteBrandCountryGroupVo parsedCountry : parsedFile.getCountries()) {
            Map<String, DeleteBrandCountryResultItemDto> processedItems = processedByCountry.get(parsedCountry.getCountry());
            if (processedItems == null) {
                throw new BusinessException("缺少国家结果: " + parsedFile.getSourceFilename() + " / " + parsedCountry.getCountry());
            }
            DeleteBrandProcessedCountryDto mergedCountry = new DeleteBrandProcessedCountryDto();
            mergedCountry.setCountry(parsedCountry.getCountry());
            for (DeleteBrandCountryAsinVo parsedAsin : parsedCountry.getItems()) {
                DeleteBrandCountryResultItemDto processed = processedItems.get(normalizeAsinKey(parsedAsin.getAsin()));
                if (processed == null) {
                    throw new BusinessException("缺少 ASIN 结果: " + parsedFile.getSourceFilename() + " / " + parsedCountry.getCountry() + " / " + parsedAsin.getAsin());
                }
                DeleteBrandCountryResultItemDto row = new DeleteBrandCountryResultItemDto();
                row.setAsin(parsedAsin.getAsin());
                row.setStatus(processed.getStatus());
                mergedCountry.getItems().add(row);
            }
            mergedCountries.add(mergedCountry);
        }

        return new MergedDeleteBrandFile(
                blankToEmpty(chunks.get(chunks.size() - 1).getCurrentCountry()),
                blankToEmpty(chunks.get(chunks.size() - 1).getCurrentAsin()),
                processedRows,
                mergedCountries
        );
    }

    private File buildResultWorkbook(Long taskId,
                                     DeleteBrandParsedFileCacheDto parsedFile,
                                     MergedDeleteBrandFile mergedFile) {
        File outputDir = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "delete-brand-result", String.valueOf(taskId)));
        File outputFile = buildNamedOutputFile(outputDir, blankToDefault(parsedFile.getSourceFilename(), "delete-brand-result.xlsx"));

        try (Workbook workbook = new XSSFWorkbook(); FileOutputStream outputStream = new FileOutputStream(outputFile)) {
            Sheet sheet = workbook.createSheet("删除品牌结果");
            Row headerRow = sheet.createRow(0);
            headerRow.createCell(0).setCellValue("国家");
            headerRow.createCell(1).setCellValue("删除ASIN");
            headerRow.createCell(2).setCellValue("状态");

            int rowIndex = 1;
            for (DeleteBrandProcessedCountryDto country : mergedFile.countries()) {
                for (DeleteBrandCountryResultItemDto item : country.getItems()) {
                    Row row = sheet.createRow(rowIndex++);
                    createTextCell(row, 0, country.getCountry());
                    createTextCell(row, 1, item.getAsin());
                    createTextCell(row, 2, item.getStatus());
                }
            }
            for (int i = 0; i < 3; i++) {
                sheet.autoSizeColumn(i);
            }
            workbook.write(outputStream);
            return outputFile;
        } catch (Exception ex) {
            throw new BusinessException("生成删除品牌结果文件失败: " + parsedFile.getSourceFilename());
        }
    }

    private File buildResultWorkbookPreserveLayout(Long taskId,
                                                   DeleteBrandParsedFileCacheDto parsedFile,
                                                   MergedDeleteBrandFile mergedFile) {
        File outputDir = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "delete-brand-result", String.valueOf(taskId)));
        File outputFile = buildNamedOutputFile(outputDir, blankToDefault(parsedFile.getSourceFilename(), "delete-brand-result.xlsx"));

        try (Workbook workbook = new XSSFWorkbook(); FileOutputStream outputStream = new FileOutputStream(outputFile)) {
            Sheet sheet = workbook.createSheet("删除品牌结果");
            Row countryRow = sheet.createRow(0);
            Row headerRow = sheet.createRow(1);

            for (int countryIndex = 0; countryIndex < mergedFile.countries().size(); countryIndex++) {
                DeleteBrandProcessedCountryDto country = mergedFile.countries().get(countryIndex);
                int asinColumnIndex = countryIndex * 2;
                int statusColumnIndex = asinColumnIndex + 1;

                createTextCell(countryRow, asinColumnIndex, country.getCountry());
                createTextCell(headerRow, asinColumnIndex, "删除ASIN");
                createTextCell(headerRow, statusColumnIndex, "状态");

                List<DeleteBrandCountryResultItemDto> items = country.getItems();
                if (items == null) {
                    continue;
                }

                for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
                    DeleteBrandCountryResultItemDto item = items.get(itemIndex);
                    Row row = sheet.getRow(itemIndex + 2);
                    if (row == null) {
                        row = sheet.createRow(itemIndex + 2);
                    }
                    createTextCell(row, asinColumnIndex, item.getAsin());
                    createTextCell(row, statusColumnIndex, item.getStatus());
                }
            }

            for (int i = 0; i < mergedFile.countries().size() * 2; i++) {
                sheet.autoSizeColumn(i);
            }
            workbook.write(outputStream);
            return outputFile;
        } catch (Exception ex) {
            throw new BusinessException("生成删除品牌结果文件失败: " + parsedFile.getSourceFilename());
        }
    }

    private void createTextCell(Row row, int index, String value) {
        Cell cell = row.createCell(index);
        cell.setCellValue(value == null ? "" : value);
    }

    private File buildNamedOutputFile(File outputDir, String filename) {
        File candidate = FileUtil.file(outputDir, filename);
        if (!candidate.exists()) {
            return candidate;
        }
        String mainName = FileUtil.mainName(filename);
        String extName = FileUtil.extName(filename);
        int index = 2;
        while (true) {
            String nextFilename = extName == null || extName.isBlank()
                    ? mainName + "_" + index
                    : mainName + "_" + index + "." + extName;
            File nextCandidate = FileUtil.file(outputDir, nextFilename);
            if (!nextCandidate.exists()) {
                return nextCandidate;
            }
            index++;
        }
    }

    public String resolveTaskDownloadUrl(Long taskId) {
        FileResultEntity entity = findLatestSuccessfulResult(taskId);
        return entity == null || entity.getResultFileUrl() == null ? null : ossStorageService.getPublicUrl(entity.getResultFileUrl());
    }

    public String resolveTaskDownloadFilename(Long taskId) {
        FileResultEntity entity = findLatestSuccessfulResult(taskId);
        return entity == null ? null : entity.getResultFilename();
    }

    private FileResultEntity findLatestSuccessfulResult(Long taskId) {
        return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                        .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                        .eq(FileResultEntity::getTaskId, taskId)
                        .eq(FileResultEntity::getSuccess, 1)
                        .orderByDesc(FileResultEntity::getId)
                        .last("limit 1"))
                .stream()
                .findFirst()
                .orElse(null);
    }

    private String blankToDefault(String value, String defaultValue) {
        String normalized = blankToEmpty(value);
        return normalized.isBlank() ? defaultValue : normalized;
    }

    private record ParsedDeleteBrandFile(int totalRows,
                                         List<DeleteBrandCountryGroupVo> countries,
                                         List<DeleteBrandPreviewRowVo> previewRows) {
    }

    private record MergedDeleteBrandFile(String currentCountry,
                                         String currentAsin,
                                         int processedRows,
                                         List<DeleteBrandProcessedCountryDto> countries) {
    }
}
