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

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.productrisk.mapper.ProductRiskShopCandidateMapper;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCreateTaskRequest;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskRowDto;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskShopPayloadDto;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskSubmitResultRequest;
import com.nanri.aiimage.modules.productrisk.model.entity.ProductRiskShopCandidateEntity;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCreateTaskVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskHistoryVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskResultItemVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskDetailVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskPendingDeleteVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskItemVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskShopQueueItemVo;
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.service.ZiniaoShopSwitchService;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ZipUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

@Service
@RequiredArgsConstructor
@Slf4j
public class ProductRiskTaskService {

    private static final String MODULE_TYPE = "PRODUCT_RISK_RESOLVE";
    private static final String CONTENT_TYPE_ZIP = "application/zip";

    private final FileTaskMapper fileTaskMapper;
    private final FileResultMapper fileResultMapper;
    private final ProductRiskShopCandidateMapper candidateMapper;
    private final ZiniaoShopSwitchService ziniaoShopSwitchService;
    private final ProductRiskExcelAssemblyService excelAssemblyService;
    private final OssStorageService ossStorageService;
    private final ObjectMapper objectMapper;

    public ProductRiskDashboardVo dashboard(Long userId) {
        if (userId == null || userId <= 0) {
            throw new BusinessException("user_id 不合法");
        }
        ProductRiskDashboardVo vo = new ProductRiskDashboardVo();
        vo.setCandidateCount(candidateMapper.selectCount(new LambdaQueryWrapper<ProductRiskShopCandidateEntity>()
                .eq(ProductRiskShopCandidateEntity::getUserId, userId)));
        Long processed = fileTaskMapper.selectCount(new LambdaQueryWrapper<FileTaskEntity>()
                .eq(FileTaskEntity::getModuleType, MODULE_TYPE)
                .eq(FileTaskEntity::getUserId, userId)
                .in(FileTaskEntity::getStatus, List.of("SUCCESS", "FAILED")));
        Long success = fileTaskMapper.selectCount(new LambdaQueryWrapper<FileTaskEntity>()
                .eq(FileTaskEntity::getModuleType, MODULE_TYPE)
                .eq(FileTaskEntity::getUserId, userId)
                .eq(FileTaskEntity::getStatus, "SUCCESS"));
        Long failed = fileTaskMapper.selectCount(new LambdaQueryWrapper<FileTaskEntity>()
                .eq(FileTaskEntity::getModuleType, MODULE_TYPE)
                .eq(FileTaskEntity::getUserId, userId)
                .eq(FileTaskEntity::getStatus, "FAILED"));
        vo.setProcessedTaskCount(processed == null ? 0L : processed);
        vo.setSuccessTaskCount(success == null ? 0L : success);
        vo.setFailedTaskCount(failed == null ? 0L : failed);
        return vo;
    }

    public ProductRiskHistoryVo listHistory(Long userId) {
        if (userId == null || userId <= 0) {
            throw new BusinessException("user_id 不合法");
        }
        ProductRiskHistoryVo vo = new ProductRiskHistoryVo();
        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, 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 t : tasks) {
                if (t != null) {
                    statusByTaskId.put(t.getId(), t.getStatus());
                }
            }
        }
        List<ProductRiskResultItemVo> items = new ArrayList<>();
        for (FileResultEntity entity : entities) {
            items.add(toHistoryItemVo(entity, statusByTaskId.get(entity.getTaskId())));
        }
        vo.setItems(items);
        return vo;
    }

    @Transactional
    public void deleteHistory(Long resultId, Long userId) {
        if (userId == null || userId <= 0) {
            throw new BusinessException("user_id 不合法");
        }
        FileResultEntity entity = fileResultMapper.selectById(resultId);
        if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
            throw new BusinessException("记录不存在");
        }
        Long taskId = entity.getTaskId();
        fileResultMapper.deleteById(resultId);
        if (taskId != null && taskId > 0) {
            reconcileTaskAfterResultRemoval(taskId);
        }
    }

    /**
     * 删除整条商品风险任务及其下所有店铺结果（运行中或已结束均可，需归属当前用户）。
     */
    @Transactional
    public void deleteTask(Long taskId, Long userId) {
        if (userId == null || userId <= 0) {
            throw new BusinessException("user_id 不合法");
        }
        FileTaskEntity task = fileTaskMapper.selectById(taskId);
        if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
            throw new BusinessException("任务不存在");
        }
        fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
                .eq(FileResultEntity::getTaskId, taskId)
                .eq(FileResultEntity::getModuleType, MODULE_TYPE));
        fileTaskMapper.deleteById(taskId);
    }

    /**
     * 从用户当前「运行中」的商品风险任务里，按规范化店铺名删除一条结果（用于前端移除匹配列表后同步后端）。
     * 若无对应记录则 removed=false。
     */
    @Transactional
    public ProductRiskPendingDeleteVo deletePendingShopResult(Long userId, String shopName) {
        if (userId == null || userId <= 0) {
            throw new BusinessException("user_id 不合法");
        }
        if (shopName == null || shopName.isBlank()) {
            throw new BusinessException("shop_name 不能为空");
        }
        String norm = ziniaoShopSwitchService.normalizeShopName(shopName);
        if (norm.isBlank()) {
            throw new BusinessException("店铺名无效");
        }
        List<FileResultEntity> candidates = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                .eq(FileResultEntity::getUserId, userId)
                .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                .eq(FileResultEntity::getSourceFilename, norm)
                .orderByDesc(FileResultEntity::getId)
                .last("limit 30"));
        ProductRiskPendingDeleteVo vo = new ProductRiskPendingDeleteVo();
        vo.setRemoved(false);
        for (FileResultEntity fr : candidates) {
            if (fr.getTaskId() == null) {
                continue;
            }
            FileTaskEntity t = fileTaskMapper.selectById(fr.getTaskId());
            if (t == null || !MODULE_TYPE.equals(t.getModuleType()) || !userId.equals(t.getUserId())) {
                continue;
            }
            if (!"RUNNING".equals(t.getStatus())) {
                continue;
            }
            Long tid = fr.getTaskId();
            fileResultMapper.deleteById(fr.getId());
            reconcileTaskAfterResultRemoval(tid);
            vo.setRemoved(true);
            return vo;
        }
        return vo;
    }

    /**
     * 删除一条 file_result 后重算父任务状态；若无剩余结果则删除任务。
     */
    private void reconcileTaskAfterResultRemoval(Long taskId) {
        FileTaskEntity task = fileTaskMapper.selectById(taskId);
        if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
            return;
        }
        List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                .eq(FileResultEntity::getTaskId, taskId)
                .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                .orderByAsc(FileResultEntity::getId));
        if (latest.isEmpty()) {
            fileTaskMapper.deleteById(taskId);
            return;
        }
        int ok = 0;
        int fail = 0;
        List<String> allErrors = new ArrayList<>();
        boolean allDone = true;
        for (FileResultEntity fr : latest) {
            boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
            boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
            if (success) {
                ok++;
            } else if (failed) {
                fail++;
                allErrors.add(fr.getSourceFilename() + ": " + fr.getErrorMessage());
            } else {
                allDone = false;
            }
        }
        task.setSourceFileCount(latest.size());
        task.setSuccessFileCount(ok);
        task.setFailedFileCount(fail);
        task.setUpdatedAt(LocalDateTime.now());
        if (!allDone) {
            task.setStatus("RUNNING");
            task.setErrorMessage(null);
            task.setFinishedAt(null);
        } else if (ok > 0 && fail == 0) {
            task.setStatus("SUCCESS");
            task.setErrorMessage(null);
            task.setFinishedAt(LocalDateTime.now());
        } else if (ok > 0) {
            task.setStatus("SUCCESS");
            task.setErrorMessage(String.join("；", allErrors));
            task.setFinishedAt(LocalDateTime.now());
        } else {
            task.setStatus("FAILED");
            task.setErrorMessage(allErrors.isEmpty() ? "全部店铺处理失败" : String.join("；", allErrors));
            task.setFinishedAt(LocalDateTime.now());
        }
        try {
            task.setResultJson(objectMapper.writeValueAsString(buildSnapshotFromDb(taskId, task.getStatus())));
        } catch (Exception ex) {
            log.warn("[product-risk] compact result json failed: {}", ex.getMessage());
        }
        fileTaskMapper.updateById(task);
    }

    public ProductRiskTaskBatchVo getTaskDetailsBatch(List<Long> taskIds) {
        ProductRiskTaskBatchVo batch = new ProductRiskTaskBatchVo();
        if (taskIds == null || taskIds.isEmpty()) {
            return batch;
        }
        for (Long taskId : taskIds) {
            if (taskId == null || taskId <= 0) {
                continue;
            }
            FileTaskEntity task = fileTaskMapper.selectById(taskId);
            if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
                batch.getMissingTaskIds().add(taskId);
                continue;
            }
            batch.getItems().add(buildTaskDetail(task));
        }
        return batch;
    }

    public String resolveResultDownloadUrl(Long resultId, Long userId) {
        FileResultEntity entity = fileResultMapper.selectById(resultId);
        if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
            throw new BusinessException("记录不存在");
        }
        if (entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()) {
            throw new BusinessException("暂无可下载文件");
        }
        return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl());
    }

    public String resolveResultDownloadFilename(Long resultId, Long userId) {
        FileResultEntity entity = fileResultMapper.selectById(resultId);
        if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
            throw new BusinessException("记录不存在");
        }
        if (entity.getResultFilename() != null && !entity.getResultFilename().isBlank()) {
            return entity.getResultFilename();
        }
        return safeFileStem(entity.getSourceFilename()) + ".zip";
    }

    @Transactional
    public ProductRiskCreateTaskVo createTask(ProductRiskCreateTaskRequest request) {
        if (request.getUserId() == null || request.getUserId() <= 0) {
            throw new BusinessException("user_id 不合法");
        }
        if (request.getItems() == null || request.getItems().isEmpty()) {
            throw new BusinessException("items 不能为空");
        }
        List<ProductRiskShopQueueItemVo> uniqueItems = dedupeQueueItems(request.getItems());
        if (uniqueItems.isEmpty()) {
            throw new BusinessException("items 不能为空");
        }
        for (ProductRiskShopQueueItemVo item : uniqueItems) {
            if (item == null || !item.isMatched()) {
                String name = item == null ? "" : Objects.toString(item.getShopName(), "");
                throw new BusinessException("存在未匹配店铺，无法创建任务: " + name);
            }
        }

        FileTaskEntity task = new FileTaskEntity();
        task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
        task.setModuleType(MODULE_TYPE);
        task.setTaskMode("PYTHON_QUEUE");
        task.setStatus("RUNNING");
        task.setSourceFileCount(uniqueItems.size());
        task.setSuccessFileCount(0);
        task.setFailedFileCount(0);
        task.setCreatedBy("user:" + request.getUserId());
        task.setUserId(request.getUserId());
        task.setCreatedAt(LocalDateTime.now());
        task.setUpdatedAt(LocalDateTime.now());
        fileTaskMapper.insert(task);

        List<ProductRiskResultItemVo> snapshot = new ArrayList<>();
        for (ProductRiskShopQueueItemVo item : uniqueItems) {
            String norm = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
            if (norm.isBlank()) {
                throw new BusinessException("店铺名无效");
            }
            FileResultEntity fr = new FileResultEntity();
            fr.setTaskId(task.getId());
            fr.setModuleType(MODULE_TYPE);
            fr.setSourceFilename(norm);
            fr.setSourceFileUrl(item.getShopId());
            fr.setUserId(request.getUserId());
            fr.setSuccess(0);
            fr.setCreatedAt(LocalDateTime.now());
            fileResultMapper.insert(fr);
            snapshot.add(toSnapshotVo(fr, item, task.getStatus()));
        }

        try {
            ProductRiskCreateTaskRequest persisted = new ProductRiskCreateTaskRequest();
            persisted.setUserId(request.getUserId());
            persisted.setItems(uniqueItems);
            task.setRequestJson(objectMapper.writeValueAsString(persisted));
            task.setResultJson(objectMapper.writeValueAsString(snapshot));
        } catch (Exception ex) {
            throw new BusinessException("序列化任务数据失败");
        }
        fileTaskMapper.updateById(task);

        ProductRiskCreateTaskVo vo = new ProductRiskCreateTaskVo();
        vo.setTaskId(task.getId());
        vo.setItems(snapshot);
        return vo;
    }

    @Transactional
    public void submitResult(Long taskId, ProductRiskSubmitResultRequest request) {
        if (taskId == null || taskId <= 0) {
            throw new BusinessException("taskId 不合法");
        }
        if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
            throw new BusinessException("shops 不能为空");
        }

        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("任务已结束，拒绝重复提交");
        }

        List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                .eq(FileResultEntity::getTaskId, taskId)
                .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                .orderByAsc(FileResultEntity::getId));

        Map<String, ProductRiskShopPayloadDto> payloadByShop = new LinkedHashMap<>();
        for (ProductRiskShopPayloadDto p : request.getShops()) {
            if (p == null) {
                continue;
            }
            String key = ziniaoShopSwitchService.normalizeShopName(p.getShopName());
            if (!key.isBlank()) {
                payloadByShop.put(key, p);
            }
        }

        File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "product-risk-result", String.valueOf(taskId)));

        List<String> batchErrors = new ArrayList<>();

        for (FileResultEntity fr : resultRows) {
            String shopKey = fr.getSourceFilename();
            ProductRiskShopPayloadDto payload = shopKey == null ? null : payloadByShop.get(shopKey);
            if (payload == null) {
                // 与删除品牌一致：支持队列逐条处理、分次回传，未在本次 payload 中的店铺保持待处理
                continue;
            }
            if (payload.getError() != null && !payload.getError().isBlank()) {
                markResultFailed(fr, payload.getError());
                batchErrors.add(shopKey + ": " + payload.getError());
                continue;
            }
            try {
                Map<String, List<ProductRiskRowDto>> countries = excelAssemblyService.normalizeCountriesMap(payload.getCountries());
                String displayName = payload.getShopName() != null && !payload.getShopName().isBlank()
                        ? payload.getShopName().trim()
                        : shopKey;
                String stem = safeFileStem(displayName);
                File xlsx = FileUtil.file(workRoot, stem + ".xlsx");
                excelAssemblyService.writeWorkbook(xlsx, displayName, countries);
                File zip = FileUtil.file(workRoot, stem + ".zip");
                ZipUtil.zip(zip, false, xlsx);
                String objectKey = ossStorageService.uploadResultFile(zip, MODULE_TYPE);
                fr.setResultFilename(stem + ".zip");
                fr.setResultFileUrl(objectKey);
                fr.setResultFileSize(zip.length());
                fr.setResultContentType(CONTENT_TYPE_ZIP);
                fr.setRowCount(excelAssemblyService.countRows(countries));
                fr.setSuccess(1);
                fr.setErrorMessage(null);
                fileResultMapper.updateById(fr);
                FileUtil.del(xlsx);
                FileUtil.del(zip);
            } catch (Exception ex) {
                log.warn("[product-risk] shop assemble failed taskId={} shop={} msg={}", taskId, shopKey, ex.getMessage());
                markResultFailed(fr, ex.getMessage() == null ? "组装失败" : ex.getMessage());
                batchErrors.add(shopKey + ": " + ex.getMessage());
            }
        }

        List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                .eq(FileResultEntity::getTaskId, taskId)
                .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                .orderByAsc(FileResultEntity::getId));

        int ok = 0;
        int fail = 0;
        List<String> allErrors = new ArrayList<>();
        boolean allDone = true;
        for (FileResultEntity fr : latest) {
            boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
            boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
            if (success) {
                ok++;
            } else if (failed) {
                fail++;
                allErrors.add(fr.getSourceFilename() + ": " + fr.getErrorMessage());
            } else {
                allDone = false;
            }
        }

        task.setSuccessFileCount(ok);
        task.setFailedFileCount(fail);
        task.setUpdatedAt(LocalDateTime.now());

        if (!allDone) {
            task.setStatus("RUNNING");
            task.setErrorMessage(null);
            task.setFinishedAt(null);
        } else if (ok > 0 && fail == 0) {
            task.setStatus("SUCCESS");
            task.setErrorMessage(null);
            task.setFinishedAt(LocalDateTime.now());
        } else if (ok > 0) {
            task.setStatus("SUCCESS");
            task.setErrorMessage(String.join("；", allErrors.isEmpty() ? batchErrors : allErrors));
            task.setFinishedAt(LocalDateTime.now());
        } else {
            task.setStatus("FAILED");
            task.setErrorMessage(allErrors.isEmpty() ? "全部店铺处理失败" : String.join("；", allErrors));
            task.setFinishedAt(LocalDateTime.now());
        }
        try {
            task.setResultJson(objectMapper.writeValueAsString(buildSnapshotFromDb(taskId, task.getStatus())));
        } catch (Exception ex) {
            log.warn("[product-risk] compact result json failed: {}", ex.getMessage());
        }
        fileTaskMapper.updateById(task);
    }

    private void markResultFailed(FileResultEntity fr, String message) {
        fr.setSuccess(0);
        fr.setErrorMessage(message);
        fr.setResultFilename(null);
        fr.setResultFileUrl(null);
        fr.setResultFileSize(0L);
        fileResultMapper.updateById(fr);
    }

    private ProductRiskTaskDetailVo buildTaskDetail(FileTaskEntity task) {
        ProductRiskTaskDetailVo detail = new ProductRiskTaskDetailVo();
        detail.setTask(toTaskItemVo(task));
        List<FileResultEntity> rows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                .eq(FileResultEntity::getTaskId, task.getId())
                .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                .orderByAsc(FileResultEntity::getId));
        for (FileResultEntity fr : rows) {
            detail.getItems().add(toHistoryItemVo(fr, task.getStatus()));
        }
        return detail;
    }

    private List<ProductRiskResultItemVo> buildSnapshotFromDb(Long taskId, String taskStatus) {
        List<FileResultEntity> rows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                .eq(FileResultEntity::getTaskId, taskId)
                .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                .orderByAsc(FileResultEntity::getId));
        List<ProductRiskResultItemVo> list = new ArrayList<>();
        for (FileResultEntity fr : rows) {
            list.add(toHistoryItemVo(fr, taskStatus));
        }
        return list;
    }

    private ProductRiskTaskItemVo toTaskItemVo(FileTaskEntity task) {
        ProductRiskTaskItemVo vo = new ProductRiskTaskItemVo();
        vo.setId(task.getId());
        vo.setTaskNo(task.getTaskNo());
        vo.setStatus(task.getStatus());
        vo.setErrorMessage(task.getErrorMessage());
        vo.setCreatedAt(fmt(task.getCreatedAt()));
        vo.setUpdatedAt(fmt(task.getUpdatedAt()));
        vo.setFinishedAt(fmt(task.getFinishedAt()));
        return vo;
    }

    private ProductRiskResultItemVo toSnapshotVo(FileResultEntity fr, ProductRiskShopQueueItemVo item, String taskStatus) {
        ProductRiskResultItemVo vo = new ProductRiskResultItemVo();
        vo.setResultId(fr.getId());
        vo.setTaskId(fr.getTaskId());
        vo.setShopName(item.getShopName());
        vo.setShopId(item.getShopId());
        vo.setPlatform(item.getPlatform());
        vo.setCompanyName(item.getCompanyName());
        vo.setMatched(item.isMatched());
        vo.setMatchStatus(item.getMatchStatus());
        vo.setMatchMessage(item.getMatchMessage());
        vo.setTaskStatus(taskStatus);
        vo.setSuccess(false);
        vo.setError(null);
        vo.setOutputFilename(null);
        vo.setDownloadUrl(null);
        return vo;
    }

    private ProductRiskResultItemVo toHistoryItemVo(FileResultEntity entity, String taskStatus) {
        ProductRiskResultItemVo vo = new ProductRiskResultItemVo();
        vo.setResultId(entity.getId());
        vo.setTaskId(entity.getTaskId());
        vo.setShopName(entity.getSourceFilename());
        vo.setShopId(entity.getSourceFileUrl());
        vo.setTaskStatus(taskStatus);
        boolean ok = entity.getSuccess() != null && entity.getSuccess() == 1;
        vo.setSuccess(ok);
        vo.setError(entity.getErrorMessage());
        vo.setOutputFilename(entity.getResultFilename());
        vo.setDownloadUrl(entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()
                ? null
                : ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()));
        vo.setMatched(true);
        if (entity.getTaskId() != null) {
            mergeQueueFieldsFromRequest(vo, entity.getTaskId());
        }
        return vo;
    }

    private void mergeQueueFieldsFromRequest(ProductRiskResultItemVo vo, Long taskId) {
        FileTaskEntity t = fileTaskMapper.selectById(taskId);
        if (t == null || t.getRequestJson() == null || t.getRequestJson().isBlank()) {
            return;
        }
        try {
            ProductRiskCreateTaskRequest req = objectMapper.readValue(t.getRequestJson(), ProductRiskCreateTaskRequest.class);
            if (req.getItems() == null) {
                return;
            }
            String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName());
            for (ProductRiskShopQueueItemVo item : req.getItems()) {
                if (item == null) {
                    continue;
                }
                if (key.equals(ziniaoShopSwitchService.normalizeShopName(item.getShopName()))) {
                    vo.setShopName(item.getShopName());
                    vo.setShopId(item.getShopId());
                    vo.setPlatform(item.getPlatform());
                    vo.setCompanyName(item.getCompanyName());
                    vo.setMatchStatus(item.getMatchStatus());
                    vo.setMatchMessage(item.getMatchMessage());
                    break;
                }
            }
        } catch (Exception ignored) {
        }
    }

    private static String fmt(LocalDateTime t) {
        return t == null ? null : t.toString();
    }

    private List<ProductRiskShopQueueItemVo> dedupeQueueItems(List<ProductRiskShopQueueItemVo> raw) {
        Map<String, ProductRiskShopQueueItemVo> byNorm = new LinkedHashMap<>();
        for (ProductRiskShopQueueItemVo item : raw) {
            if (item == null) {
                continue;
            }
            String k = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
            if (k.isBlank()) {
                continue;
            }
            byNorm.putIfAbsent(k, item);
        }
        return new ArrayList<>(byNorm.values());
    }

    private static String safeFileStem(String shopName) {
        String s = shopName == null ? "shop" : shopName.trim();
        if (s.isBlank()) {
            s = "shop";
        }
        return s.replaceAll("[\\\\/:*?\"<>|]", "_");
    }
}
