package com.cmvr.inspection.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
import com.cmvr.common.exception.ServiceException;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.device.service.InspectionAlertListenService;
import com.cmvr.framework.websocket.service.MessagePushService;
import com.cmvr.inspection.domain.InspectionAlarm;
import com.cmvr.inspection.domain.InspectionDetectionAlert;
import com.cmvr.inspection.domain.InspectionResult;
import com.cmvr.inspection.domain.InspectionResultMedia;
import com.cmvr.inspection.domain.InspectionRobot;
import com.cmvr.inspection.domain.InspectionTask;
import com.cmvr.inspection.domain.InspectionTaskInstance;
import com.cmvr.inspection.domain.InspectionTaskLog;
import com.cmvr.inspection.domain.dto.InspectionResultQuery;
import com.cmvr.inspection.domain.dto.InspectionResultReviewRequest;
import com.cmvr.inspection.domain.vo.InspectionReportSummaryVo;
import com.cmvr.inspection.domain.vo.InspectionResultVo;
import com.cmvr.inspection.enums.InspectionLogTypeEnum;
import com.cmvr.inspection.enums.TaskStatusEnum;
import com.cmvr.inspection.mapper.InspectionAlarmMapper;
import com.cmvr.inspection.mapper.InspectionResultMapper;
import com.cmvr.inspection.mapper.InspectionResultMediaMapper;
import com.cmvr.inspection.mapper.InspectionRobotMapper;
import com.cmvr.inspection.mapper.InspectionTaskMapper;
import com.cmvr.inspection.mapper.InspectionTaskInstanceMapper;
import com.cmvr.inspection.service.IInspectionResultService;
import com.cmvr.inspection.service.IInspectionTaskLogService;
import com.cmvr.test.flow.runtime.event.FlowExecutionEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 统一巡检结果业务实现。
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class InspectionResultServiceImpl extends ServiceImpl<InspectionResultMapper, InspectionResult>
        implements IInspectionResultService
{
    private static final String RESULT_MARKER = "_inspectionResult";
    private static final String STATUS_PENDING = "PENDING";
    private static final String STATUS_NORMAL = "NORMAL";
    private static final String STATUS_ABNORMAL = "ABNORMAL";
    private static final String STATUS_RECOGNIZE_FAILED = "RECOGNIZE_FAILED";

    private final InspectionResultMediaMapper mediaMapper;
    private final InspectionAlarmMapper alarmMapper;
    private final InspectionTaskMapper taskMapper;
    private final InspectionTaskInstanceMapper taskInstanceMapper;
    private final InspectionRobotMapper robotMapper;
    private final IInspectionTaskLogService taskLogService;
    private final MessagePushService messagePushService;

    @Override
    public List<InspectionResultVo> selectResultList(InspectionResultQuery query)
    {
        InspectionResultQuery condition = query == null ? new InspectionResultQuery() : query;
        LambdaQueryWrapper<InspectionResult> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(StringUtils.isNotBlank(condition.getTaskInstanceId()),
                        InspectionResult::getTaskInstanceId, condition.getTaskInstanceId())
                .eq(StringUtils.isNotBlank(condition.getTaskId()),
                        InspectionResult::getTaskId, condition.getTaskId())
                .eq(StringUtils.isNotBlank(condition.getItemId()),
                        InspectionResult::getItemId, condition.getItemId())
                .eq(StringUtils.isNotBlank(condition.getResultType()),
                        InspectionResult::getResultType, condition.getResultType())
                .eq(StringUtils.isNotBlank(condition.getResultStatus()),
                        InspectionResult::getResultStatus, condition.getResultStatus())
                .eq(condition.getAlarmLevel() != null,
                        InspectionResult::getAlarmLevel, condition.getAlarmLevel())
                .like(StringUtils.isNotBlank(condition.getResultName()),
                        InspectionResult::getResultName, condition.getResultName())
                .orderByDesc(InspectionResult::getOccurredTime, InspectionResult::getCreateTime);
        return toResultVoList(list(wrapper));
    }

    @Override
    public InspectionResultVo selectResultById(String id)
    {
        InspectionResult result = getById(id);
        if (result == null)
        {
            return null;
        }
        InspectionResultVo vo = new InspectionResultVo();
        BeanUtils.copyProperties(result, vo);
        if (StringUtils.isNotBlank(result.getTaskId()))
        {
            InspectionTask task = taskMapper.selectById(result.getTaskId());
            vo.setTaskName(task == null ? null : task.getTaskName());
        }
        List<InspectionResultMedia> mediaList = mediaMapper.selectList(new LambdaQueryWrapper<InspectionResultMedia>()
                .eq(InspectionResultMedia::getResultId, id)
                .orderByAsc(InspectionResultMedia::getSortOrder));
        vo.setMediaList(mediaList);
        return vo;
    }

    /**
     * 批量补充任务名称并转换为列表视图，避免按结果逐条查询任务表。
     */
    private List<InspectionResultVo> toResultVoList(List<InspectionResult> results)
    {
        if (results == null || results.isEmpty())
        {
            return Collections.emptyList();
        }

        Set<String> taskIds = results.stream()
                .map(InspectionResult::getTaskId)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toCollection(HashSet::new));
        Map<String, InspectionTask> taskMap = taskIds.isEmpty()
                ? Collections.emptyMap()
                : taskMapper.selectBatchIds(taskIds).stream()
                        .collect(Collectors.toMap(InspectionTask::getId, Function.identity(), (left, right) -> left));

        List<InspectionResultVo> resultVos = new ArrayList<>(results.size());
        for (InspectionResult result : results)
        {
            InspectionResultVo vo = new InspectionResultVo();
            BeanUtils.copyProperties(result, vo);
            InspectionTask task = taskMap.get(result.getTaskId());
            vo.setTaskName(task == null ? null : task.getTaskName());
            resultVos.add(vo);
        }
        return resultVos;
    }

    /**
     * 提交人工复核。使用reviewVersion做条件更新，保证并发审核只有一个请求成功。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public InspectionResult review(String id, InspectionResultReviewRequest request)
    {
        if (request == null)
        {
            throw new ServiceException("复核参数不能为空");
        }
        String targetStatus = StringUtils.upperCase(StringUtils.trimToEmpty(request.getResultStatus()));
        if (!STATUS_NORMAL.equals(targetStatus) && !STATUS_ABNORMAL.equals(targetStatus))
        {
            throw new ServiceException("人工复核结果只能为NORMAL或ABNORMAL");
        }
        if (STATUS_ABNORMAL.equals(targetStatus)
                && (request.getAlarmLevel() == null || request.getAlarmLevel() < 1 || request.getAlarmLevel() > 3))
        {
            throw new ServiceException("异常结果必须指定1到3级告警级别");
        }

        InspectionResult current = getById(id);
        if (current == null)
        {
            throw new ServiceException("巡检结果不存在");
        }
        if (!STATUS_PENDING.equals(current.getResultStatus())
                && !STATUS_RECOGNIZE_FAILED.equals(current.getResultStatus()))
        {
            throw new ServiceException("该巡检结果已完成复核，不能重复提交");
        }

        int expectedVersion = request.getReviewVersion() == null
                ? valueOrZero(current.getReviewVersion()) : request.getReviewVersion();
        String username = SecurityUtils.getUsername();
        Date now = DateUtils.getNowDate();
        LambdaUpdateWrapper<InspectionResult> update = new LambdaUpdateWrapper<>();
        update.eq(InspectionResult::getId, id)
                .eq(InspectionResult::getReviewVersion, expectedVersion)
                .set(InspectionResult::getResultStatus, targetStatus)
                .set(InspectionResult::getAlarmLevel,
                        STATUS_ABNORMAL.equals(targetStatus) ? request.getAlarmLevel() : null)
                .set(InspectionResult::getAlarmMessage,
                        STATUS_ABNORMAL.equals(targetStatus) ? "人工复核判定异常" : null)
                .set(InspectionResult::getReviewer, username)
                .set(InspectionResult::getReviewTime, now)
                .set(InspectionResult::getReviewRemark, request.getReviewRemark())
                .set(InspectionResult::getReviewVersion, expectedVersion + 1)
                .set(InspectionResult::getUpdateBy, username)
                .set(InspectionResult::getUpdateTime, now);
        if (!update(update))
        {
            throw new ServiceException("巡检结果已被其他人员修改，请刷新后重试");
        }

        InspectionResult reviewed = getById(id);
        if (STATUS_ABNORMAL.equals(reviewed.getResultStatus()))
        {
            createAlarmIfNecessary(reviewed);
        }
        appendResultLog(reviewed, "人工复核完成");
        pushResult(reviewed);
        return reviewed;
    }

    @Override
    public InspectionReportSummaryVo buildReportSummary(String taskInstanceId)
    {
        List<InspectionResult> results = list(new LambdaQueryWrapper<InspectionResult>()
                .eq(InspectionResult::getTaskInstanceId, taskInstanceId));
        InspectionReportSummaryVo summary = new InspectionReportSummaryVo();
        summary.setTaskInstanceId(taskInstanceId);
        summary.setTotalCount(results.size());

        Map<Integer, Long> levelCounts = new LinkedHashMap<>();
        levelCounts.put(1, 0L);
        levelCounts.put(2, 0L);
        levelCounts.put(3, 0L);
        Map<String, Long> typeCounts = new LinkedHashMap<>();
        for (InspectionResult result : results)
        {
            switch (StringUtils.defaultString(result.getResultStatus()))
            {
                case STATUS_NORMAL:
                    summary.setNormalCount(summary.getNormalCount() + 1);
                    break;
                case STATUS_ABNORMAL:
                    summary.setAbnormalCount(summary.getAbnormalCount() + 1);
                    break;
                case STATUS_PENDING:
                    summary.setPendingCount(summary.getPendingCount() + 1);
                    break;
                case STATUS_RECOGNIZE_FAILED:
                    summary.setRecognizeFailedCount(summary.getRecognizeFailedCount() + 1);
                    break;
                default:
                    break;
            }
            if (result.getAlarmLevel() != null)
            {
                levelCounts.put(result.getAlarmLevel(), levelCounts.getOrDefault(result.getAlarmLevel(), 0L) + 1);
            }
            String type = StringUtils.defaultIfBlank(result.getResultType(), "UNKNOWN");
            typeCounts.put(type, typeCounts.getOrDefault(type, 0L) + 1);
        }
        summary.setAlarmLevelCounts(levelCounts);
        summary.setResultTypeCounts(typeCounts);
        summary.setReviewCompleted(summary.getPendingCount() == 0 && summary.getRecognizeFailedCount() == 0);
        return summary;
    }

    /**
     * 消费工作流节点的标准巡检输出。普通节点没有_inspectionResult时直接忽略。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public InspectionResult recordWorkflowResult(FlowExecutionEvent event, InspectionTaskInstance taskInstance)
    {
        if (event == null || event.getOutputParams() == null)
        {
            return null;
        }
        JSONObject source = event.getOutputParams().getJSONObject(RESULT_MARKER);
        if (source == null)
        {
            return null;
        }

        String iterationPath = JSON.toJSONString(event.getIterations() == null
                ? Collections.emptyList() : event.getIterations());
        String dedupeKey = limit("WF|" + event.getInstId() + "|" + event.getItemId() + "|"
                + event.getNodeId() + "|" + iterationPath, 255);
        InspectionResult existing = findByDedupeKey(dedupeKey);
        if (existing != null)
        {
            return existing;
        }

        InspectionResult result = baseResult(taskInstance, event.getInstId(), event.getItemId(), event.getNodeId());
        result.setDedupeKey(dedupeKey);
        result.setNodeName(event.getNodeName());
        result.setIterationPath(iterationPath);
        result.setResultType(upper(source.getString("resultType")));
        result.setResultStatus(upper(source.getString("resultStatus")));
        result.setResultName(StringUtils.defaultIfBlank(source.getString("resultName"), event.getNodeName()));
        result.setValueNumber(source.getBigDecimal("valueNumber"));
        result.setValueText(source.getString("valueText"));
        result.setUnit(source.getString("unit"));
        result.setAlarmLevel(source.getInteger("alarmLevel"));
        result.setAlarmMessage(source.getString("alarmMessage"));
        result.setConfidence(source.getBigDecimal("confidence"));
        result.setEvidenceUrl(source.getString("evidenceUrl"));
        result.setEvidenceType(resolveSourceEvidenceType(source));
        result.setRawResult(toJsonText(source.get("rawResult")));
        result.setMatchedRuleJson(toJsonText(source.get("matchedRule")));
        result.setRuleSnapshotJson(toJsonText(source.get("ruleSnapshot")));
        validateStandardResult(result);

        try
        {
            insertResult(result);
        }
        catch (DuplicateKeyException ex)
        {
            return findByDedupeKey(dedupeKey);
        }
        saveMedia(result.getId(), source);
        postCreate(result, "巡检节点产生结果");
        return result;
    }

    /**
     * 将一条PPE原始报警按命中的工作流订阅转换为巡检结果。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public InspectionResult recordPpeResult(InspectionDetectionAlert alert,
                                            InspectionAlertListenService.ListenSubscription subscription,
                                            InspectionTaskInstance taskInstance)
    {
        String dedupeKey = limit("PPE|" + alert.getEventId() + "|" + subscription.getSubscriptionId(), 255);
        InspectionResult existing = findByDedupeKey(dedupeKey);
        if (existing != null)
        {
            return existing;
        }

        InspectionResult result = baseResult(taskInstance, subscription.getFlowInstanceId(),
                subscription.getItemId(), subscription.getNodeId());
        result.setDedupeKey(dedupeKey);
        result.setResultType("PPE");
        result.setResultStatus(STATUS_ABNORMAL);
        result.setResultName(ppeName(subscription.getEventType()));
        result.setValueText(subscription.getEventType());
        result.setAlarmLevel("No-Helmet".equals(subscription.getEventType()) ? 3 : 2);
        result.setAlarmMessage(ppeMessage(subscription.getEventType()));
        result.setConfidence(alert.getMaxConfidence() == null
                ? null : BigDecimal.valueOf(alert.getMaxConfidence()));
        result.setSourceEventId(alert.getEventId());
        result.setEvidenceUrl(alert.getImagePath());
        result.setEvidenceType(StringUtils.isBlank(alert.getImagePath()) ? null : "IMAGE");
        result.setRawResult(alert.getRawJson());
        result.setOccurredTime(fromUnixNs(alert.getTriggeredAtNs()));

        try
        {
            insertResult(result);
        }
        catch (DuplicateKeyException ex)
        {
            return findByDedupeKey(dedupeKey);
        }
        if (StringUtils.isNotBlank(result.getEvidenceUrl()))
        {
            insertMedia(result.getId(), "IMAGE", result.getEvidenceUrl(), 0);
        }
        postCreate(result, "接收到PPE违规事件");
        return result;
    }

    private InspectionResult baseResult(InspectionTaskInstance taskInstance, String flowInstanceId,
                                        String itemId, String nodeId)
    {
        InspectionResult result = new InspectionResult();
        result.setId(newId());
        result.setResultCode("IR" + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())
                + result.getId().substring(0, 5).toUpperCase(Locale.ROOT));
        result.setFlowInstanceId(flowInstanceId);
        result.setItemId(itemId);
        result.setNodeId(nodeId);
        if (taskInstance != null)
        {
            result.setTaskInstanceId(taskInstance.getId());
            result.setTaskId(taskInstance.getTaskId());
        }
        result.setReviewVersion(0);
        result.setOccurredTime(DateUtils.getNowDate());
        result.setCreateBy("system");
        result.setCreateTime(DateUtils.getNowDate());
        return result;
    }

    private void insertResult(InspectionResult result)
    {
        if (baseMapper.insert(result) != 1)
        {
            throw new ServiceException("保存巡检结果失败");
        }
    }

    private void postCreate(InspectionResult result, String logPrefix)
    {
        if (STATUS_ABNORMAL.equals(result.getResultStatus()))
        {
            createAlarmIfNecessary(result);
        }
        appendResultLog(result, logPrefix);
        pushResult(result);
    }

    /**
     * 为异常结果幂等创建巡检告警。数据库result_id唯一索引是并发场景的最终保障。
     */
    private void createAlarmIfNecessary(InspectionResult result)
    {
        InspectionAlarm existing = alarmMapper.selectOne(new LambdaQueryWrapper<InspectionAlarm>()
                .eq(InspectionAlarm::getResultId, result.getId()).last("limit 1"));
        if (existing != null)
        {
            if (!StringUtils.equals(existing.getId(), result.getAlarmId()))
            {
                result.setAlarmId(existing.getId());
                baseMapper.updateById(result);
            }
            return;
        }

        InspectionTask task = StringUtils.isBlank(result.getTaskId())
                ? null : taskMapper.selectById(result.getTaskId());
        InspectionTaskInstance taskInstance = null;
        if (StringUtils.isNotBlank(result.getTaskInstanceId()))
        {
            taskInstance = new InspectionTaskInstance();
            taskInstance.setId(result.getTaskInstanceId());
        }
        InspectionRobot robot = null;
        if (taskInstance != null)
        {
            // 仅在需要构建告警时查询完整任务实例。
            taskInstance = taskInstanceMapper.selectById(result.getTaskInstanceId());
            if (taskInstance != null && StringUtils.isNotBlank(taskInstance.getRobotId()))
            {
                robot = robotMapper.selectById(taskInstance.getRobotId());
            }
        }

        InspectionAlarm alarm = new InspectionAlarm();
        alarm.setId(newId());
        alarm.setAlarmCode("IA" + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())
                + alarm.getId().substring(0, 4).toUpperCase(Locale.ROOT));
        alarm.setResultId(result.getId());
        alarm.setAlarmSource(result.getResultType());
        alarm.setItemId(result.getItemId());
        alarm.setNodeId(result.getNodeId());
        alarm.setTaskInstanceId(result.getTaskInstanceId());
        alarm.setTaskId(result.getTaskId());
        alarm.setTaskName(task == null ? null : task.getTaskName());
        alarm.setRobotId(taskInstance == null ? null : taskInstance.getRobotId());
        alarm.setRobotName(robot == null ? null : robot.getRobotName());
        alarm.setAlarmLevel(String.valueOf(result.getAlarmLevel() == null ? 2 : result.getAlarmLevel()));
        alarm.setAlarmType("METER".equals(result.getResultType()) ? "1" : "2");
        alarm.setAlarmTitle(result.getResultName() + "异常");
        alarm.setAlarmContent(buildAlarmContent(result));
        alarm.setAlarmTime(result.getOccurredTime());
        alarm.setHandleStatus("0");
        alarm.setEvidenceImage(result.getEvidenceUrl());
        alarm.setCreateBy("system");
        alarm.setCreateTime(DateUtils.getNowDate());
        alarmMapper.insert(alarm);

        result.setAlarmId(alarm.getId());
        baseMapper.updateById(result);
        try
        {
            messagePushService.pushToChannel("InspectionAlarm", alarm);
        }
        catch (Exception ex)
        {
            log.info("巡检告警无订阅者，alarmId={}", alarm.getId());
        }
    }

    private String buildAlarmContent(InspectionResult result)
    {
        if ("METER".equals(result.getResultType()))
        {
            return result.getResultName() + "读数为" + StringUtils.defaultString(result.getValueText())
                    + StringUtils.defaultString(result.getUnit()) + "，触发" + result.getAlarmLevel()
                    + "级告警：" + StringUtils.defaultString(result.getAlarmMessage());
        }
        return StringUtils.defaultIfBlank(result.getAlarmMessage(), result.getResultName() + "检测异常");
    }

    /** 将业务结果追加到现有巡检任务时间线，并通知任务详情订阅者。 */
    private void appendResultLog(InspectionResult result, String prefix)
    {
        if (StringUtils.isBlank(result.getTaskInstanceId()))
        {
            log.warn("巡检结果未绑定任务实例，跳过任务日志，resultId={}，flowInstanceId={}",
                    result.getId(), result.getFlowInstanceId());
            return;
        }
        InspectionTaskLog taskLog = InspectionTaskLog.builder()
                .id(newId())
                .taskInstanceId(result.getTaskInstanceId())
                .taskId(result.getTaskId())
                .itemId(result.getItemId())
                .nodeId(result.getNodeId())
                .nodeName(result.getNodeName())
                .logType(resolveLogType(result.getEvidenceUrl()))
                .logContent(prefix + "：" + resultDescription(result))
                .mediaUrl(result.getEvidenceUrl())
                .status(TaskStatusEnum.SUCCESS.getCode())
                .logTime(DateUtils.getNowDate())
                .extraInfo(JSON.toJSONString(Collections.singletonMap("resultId", result.getId())))
                .build();
        taskLogService.insertInspectionTaskLog(taskLog);
        try
        {
            messagePushService.pushToChannel("InspectionTaskInstance", taskLog);
        }
        catch (Exception ex)
        {
            log.info("巡检结果日志无订阅者，resultId={}", result.getId());
        }
    }

    private String resultDescription(InspectionResult result)
    {
        if ("METER".equals(result.getResultType()))
        {
            return result.getResultName() + "=" + StringUtils.defaultString(result.getValueText())
                    + StringUtils.defaultString(result.getUnit()) + "，结果=" + result.getResultStatus()
                    + (result.getAlarmLevel() == null ? "" : "，告警级别=" + result.getAlarmLevel());
        }
        return result.getResultName() + "，结果=" + result.getResultStatus();
    }

    private void pushResult(InspectionResult result)
    {
        try
        {
            messagePushService.pushToChannel("InspectionResult", result);
        }
        catch (Exception ex)
        {
            log.info("巡检结果无订阅者，resultId={}", result.getId());
        }
    }

    /** 保存人工巡检的多媒体证据；只有单一证据时也统一落媒体表。 */
    private void saveMedia(String resultId, JSONObject source)
    {
        JSONArray mediaList = source.getJSONArray("mediaList");
        int order = 0;
        if (mediaList != null)
        {
            for (Object item : mediaList)
            {
                if (item instanceof JSONObject)
                {
                    JSONObject media = (JSONObject) item;
                    insertMedia(resultId, upper(media.getString("mediaType")), media.getString("mediaUrl"), order++);
                }
                else if (item != null)
                {
                    String url = String.valueOf(item);
                    insertMedia(resultId, detectMediaType(url), url, order++);
                }
            }
        }
        if (order == 0 && StringUtils.isNotBlank(source.getString("evidenceUrl")))
        {
            insertMedia(resultId, detectMediaType(source.getString("evidenceUrl")),
                    source.getString("evidenceUrl"), 0);
        }
    }

    private void insertMedia(String resultId, String mediaType, String mediaUrl, int order)
    {
        if (StringUtils.isBlank(mediaUrl))
        {
            return;
        }
        InspectionResultMedia media = new InspectionResultMedia();
        media.setId(newId());
        media.setResultId(resultId);
        media.setMediaType(normalizeEvidenceType(
                StringUtils.defaultIfBlank(mediaType, detectMediaType(mediaUrl))));
        media.setMediaUrl(mediaUrl);
        media.setSortOrder(order);
        media.setCreateTime(DateUtils.getNowDate());
        mediaMapper.insert(media);
    }

    private InspectionResult findByDedupeKey(String dedupeKey)
    {
        return baseMapper.selectOne(new LambdaQueryWrapper<InspectionResult>()
                .eq(InspectionResult::getDedupeKey, dedupeKey).last("limit 1"));
    }

    private void validateStandardResult(InspectionResult result)
    {
        if (!("PPE".equals(result.getResultType()) || "METER".equals(result.getResultType())
                || "MANUAL".equals(result.getResultType())))
        {
            throw new ServiceException("工作流巡检结果类型不支持: " + result.getResultType());
        }
        if (!(STATUS_PENDING.equals(result.getResultStatus()) || STATUS_NORMAL.equals(result.getResultStatus())
                || STATUS_ABNORMAL.equals(result.getResultStatus())
                || STATUS_RECOGNIZE_FAILED.equals(result.getResultStatus())))
        {
            throw new ServiceException("工作流巡检结果状态不支持: " + result.getResultStatus());
        }
        if (STATUS_ABNORMAL.equals(result.getResultStatus())
                && (result.getAlarmLevel() == null || result.getAlarmLevel() < 1 || result.getAlarmLevel() > 3))
        {
            throw new ServiceException("异常巡检结果必须指定1到3级告警级别");
        }
    }

    private int resolveLogType(String url)
    {
        String type = detectMediaType(url);
        if ("VIDEO".equals(type))
        {
            return InspectionLogTypeEnum.VIDEO.getCode();
        }
        if ("IMAGE".equals(type))
        {
            return InspectionLogTypeEnum.IMAGE.getCode();
        }
        return InspectionLogTypeEnum.TEXT.getCode();
    }

    private String detectMediaType(String url)
    {
        String normalized = StringUtils.lowerCase(StringUtils.defaultString(url));
        if (normalized.matches(".*\\.(mp4|avi|mov|mkv|webm)(\\?.*)?$"))
        {
            return "VIDEO";
        }
        return "IMAGE";
    }

    /**
     * 在结果首次入库时确定主要证据类型，列表查询无需再访问媒体明细表。
     */
    private String resolveSourceEvidenceType(JSONObject source)
    {
        String explicitType = normalizeEvidenceType(source.getString("evidenceType"));
        if (StringUtils.isNotBlank(explicitType))
        {
            return explicitType;
        }

        // 人工巡检以mediaList第一条作为主要证据，和evidenceUrl的赋值规则保持一致。
        JSONArray mediaList = source.getJSONArray("mediaList");
        if (mediaList != null && !mediaList.isEmpty() && mediaList.get(0) instanceof JSONObject)
        {
            String firstType = normalizeEvidenceType(mediaList.getJSONObject(0).getString("mediaType"));
            if (StringUtils.isNotBlank(firstType))
            {
                return firstType;
            }
        }

        String evidenceUrl = source.getString("evidenceUrl");
        return StringUtils.isBlank(evidenceUrl) ? null : detectMediaType(evidenceUrl);
    }

    /** 媒体类型只允许IMAGE或VIDEO，避免主表和媒体明细出现不一致的自由文本。 */
    private String normalizeEvidenceType(String value)
    {
        String normalized = upper(value);
        if (StringUtils.isBlank(normalized))
        {
            return null;
        }
        if (!"IMAGE".equals(normalized) && !"VIDEO".equals(normalized))
        {
            throw new ServiceException("不支持的巡检证据类型: " + value);
        }
        return normalized;
    }

    private String ppeName(String eventType)
    {
        return "No-Helmet".equals(eventType) ? "未佩戴安全帽" : "未佩戴手套";
    }

    private String ppeMessage(String eventType)
    {
        return "No-Helmet".equals(eventType) ? "检测到人员未佩戴安全帽" : "检测到人员未佩戴手套";
    }

    private Date fromUnixNs(Long timestampNs)
    {
        return timestampNs == null ? DateUtils.getNowDate() : new Date(timestampNs / 1_000_000L);
    }

    private String toJsonText(Object value)
    {
        if (value == null)
        {
            return null;
        }
        return value instanceof String ? String.valueOf(value) : JSON.toJSONString(value);
    }

    private String upper(String value)
    {
        return StringUtils.upperCase(StringUtils.trimToEmpty(value));
    }

    private int valueOrZero(Integer value)
    {
        return value == null ? 0 : value;
    }

    private String limit(String value, int maxLength)
    {
        return value.length() <= maxLength ? value : value.substring(0, maxLength);
    }

    private String newId()
    {
        return UUID.randomUUID().toString().replace("-", "");
    }
}
