package cn.nianxx.thhotel.platform.debug.service.impl;

import cn.nianxx.thhotel.integrations.ai.superagent.common.request.SuperAgentMailDebugRequest;
import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentOpenApiResult;
import cn.nianxx.thhotel.integrations.ai.superagent.service.SuperAgentOpenApiClient;
import cn.nianxx.thhotel.integrations.ai.superagent.service.impl.SuperAgentOpenApiException;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStoragePutRequest;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStoragePutResult;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.impl.AliyunOssProperties;
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.impl.ObjectStorageException;
import cn.nianxx.thhotel.platform.debug.common.dto.DebugEmlSuperAgentRunDraft;
import cn.nianxx.thhotel.platform.debug.common.dto.DebugEmlSuperAgentRunStatusUpdate;
import cn.nianxx.thhotel.platform.debug.common.dto.DebugEmlSuperAgentRunUpdate;
import cn.nianxx.thhotel.platform.debug.common.enums.DebugEmlSuperAgentRunStatus;
import cn.nianxx.thhotel.platform.debug.common.result.DebugEmlSuperAgentRunResult;
import cn.nianxx.thhotel.platform.debug.common.result.DebugEmlUploadedMediaResult;
import cn.nianxx.thhotel.platform.debug.repository.DebugEmlSuperAgentRunRepository;
import cn.nianxx.thhotel.platform.debug.service.DebugEmlSuperAgentRunService;
import cn.nianxx.thhotel.platform.message.common.dto.ParsedEmlMediaItem;
import cn.nianxx.thhotel.platform.message.common.dto.ParsedEmlMessage;
import cn.nianxx.thhotel.platform.message.common.enums.SourceMessageMediaType;
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageMedia;
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
import cn.nianxx.thhotel.platform.message.service.EmlMessageParseService;
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
import cn.nianxx.thhotel.platform.message.service.SourceMessageHtmlSanitizerService;
import cn.nianxx.thhotel.platform.message.service.impl.EmlMessageParseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

/**
 * Debug EML 上传到 SuperAgent 服务实现。编排解析、OSS、SourceMessage 和 SuperAgent 调用。
 */
@Service
public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunService {

    private static final String SOURCE_PROVIDER = "DEBUG_EML_UPLOAD";
    private static final String SOURCE_CHANNEL = "EMAIL";
    private static final String SCHEMA_VERSION = "debug-eml-upload-v1";
    private static final DateTimeFormatter DATE_FOLDER_FORMATTER = DateTimeFormatter.BASIC_ISO_DATE;
    private static final Pattern CID_REFERENCE_PATTERN = Pattern.compile(
            "(?i)cid:(?:<[^>]+>|%3c[^\\s\"'>]+%3e|[^\\s\"'>]+)");

    private final DebugEmlSuperAgentProperties properties;
    private final AliyunOssProperties ossProperties;
    private final EmlMessageParseService parseService;
    private final ObjectStorageService objectStorageService;
    private final SourceMessageCaptureService sourceMessageCaptureService;
    private final SourceMessageHtmlSanitizerService htmlSanitizerService;
    private final SuperAgentOpenApiClient superAgentOpenApiClient;
    private final DebugEmlSuperAgentRunRepository runRepository;
    private final ObjectMapper objectMapper;

    /**
     * 注入 Debug EML 所需的内部服务和外部端口。
     */
    public DebugEmlSuperAgentRunServiceImpl(
            DebugEmlSuperAgentProperties properties,
            AliyunOssProperties ossProperties,
            EmlMessageParseService parseService,
            ObjectStorageService objectStorageService,
            SourceMessageCaptureService sourceMessageCaptureService,
            SourceMessageHtmlSanitizerService htmlSanitizerService,
            SuperAgentOpenApiClient superAgentOpenApiClient,
            DebugEmlSuperAgentRunRepository runRepository,
            ObjectMapper objectMapper) {
        this.properties = properties;
        this.ossProperties = ossProperties;
        this.parseService = parseService;
        this.objectStorageService = objectStorageService;
        this.sourceMessageCaptureService = sourceMessageCaptureService;
        this.htmlSanitizerService = htmlSanitizerService;
        this.superAgentOpenApiClient = superAgentOpenApiClient;
        this.runRepository = runRepository;
        this.objectMapper = objectMapper;
    }

    /**
     * 处理单封 Debug EML 上传；第一版只展示 SuperAgent 结果，不创建订单或任务。
     */
    @Override
    public DebugEmlSuperAgentRunResult uploadAndRun(
            String accessKey,
            MultipartFile file,
            String hotelId,
            String runLabel) {
        validateAccessKey(accessKey);
        String normalizedHotelId = requireText(hotelId, "hotel_id");
        validateFile(file);
        byte[] emlBytes = readFileBytes(file);
        String safeFileName = safeFileName(file.getOriginalFilename(), "debug-email.eml");
        if (!safeFileName.toLowerCase(Locale.ROOT).endsWith(".eml")) {
            throw new DebugEmlSuperAgentException(HttpStatus.BAD_REQUEST, "INVALID_FILE_TYPE", "只支持上传 .eml 邮件文件。");
        }

        LocalDateTime now = nowUtc();
        Long runId = runRepository.insert(new DebugEmlSuperAgentRunDraft(
                normalizedHotelId,
                trimToNull(runLabel),
                DebugEmlSuperAgentRunStatus.CREATED.name(),
                now));
        try {
            return doUploadAndRun(runId, normalizedHotelId, trimToNull(runLabel), safeFileName, emlBytes, now);
        } catch (DebugEmlSuperAgentException exception) {
            markFailed(runId, exception.getMessage(), statusForException(exception), nowUtc());
            throw exception;
        } catch (EmlMessageParseException exception) {
            markFailed(runId, "EML 邮件解析失败。", DebugEmlSuperAgentRunStatus.FAILED, nowUtc());
            throw new DebugEmlSuperAgentException(
                    HttpStatus.UNPROCESSABLE_ENTITY,
                    "EML_PARSE_FAILED",
                    "EML 邮件解析失败。",
                    exception);
        } catch (ObjectStorageException exception) {
            markFailed(runId, "OSS 上传失败。", DebugEmlSuperAgentRunStatus.FAILED, nowUtc());
            throw new DebugEmlSuperAgentException(
                    HttpStatus.BAD_GATEWAY,
                    "OSS_UPLOAD_FAILED",
                    "OSS 上传失败。",
                    exception);
        } catch (SuperAgentOpenApiException exception) {
            markFailed(runId, "SuperAgent 调用失败。", DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED, nowUtc());
            throw new DebugEmlSuperAgentException(
                    HttpStatus.BAD_GATEWAY,
                    "SUPERAGENT_OPEN_API_FAILED",
                    "SuperAgent 调用失败。",
                    exception);
        } catch (Exception exception) {
            markFailed(runId, "Debug EML 上传处理失败。", DebugEmlSuperAgentRunStatus.FAILED, nowUtc());
            throw new DebugEmlSuperAgentException(
                    HttpStatus.INTERNAL_SERVER_ERROR,
                    "DEBUG_EML_RUN_FAILED",
                    "Debug EML 上传处理失败。",
                    exception);
        }
    }

    /**
     * 执行已创建 runId 的主流程。
     */
    private DebugEmlSuperAgentRunResult doUploadAndRun(
            Long runId,
            String hotelId,
            String runLabel,
            String safeFileName,
            byte[] emlBytes,
            LocalDateTime createdAt) throws Exception {
        String sha256 = sha256(emlBytes);
        ParsedEmlMessage parsed = parseService.parse(emlBytes, safeFileName);
        String originalMessageId = parsed.messageId();
        String originalConversationId = parsed.conversationId();
        String externalMessageId = debugExternalMessageId(runId, sha256);
        String externalConversationId = firstNonBlank(originalConversationId, "debug-eml-thread-" + externalMessageId);
        List<String> warnings = new ArrayList<>();
        List<UploadedMedia> uploadedMedia = new ArrayList<>();

        uploadedMedia.add(uploadOriginalEml(runId, safeFileName, emlBytes, createdAt));
        int inlineIndex = 1;
        int attachmentIndex = 1;
        for (ParsedEmlMediaItem mediaItem : parsed.mediaItems()) {
            if (SourceMessageMediaType.INLINE_IMAGE.code().equals(mediaItem.mediaType())) {
                uploadedMedia.add(uploadParsedMedia(runId, createdAt, mediaItem, "inline", inlineIndex++));
            } else {
                uploadedMedia.add(uploadParsedMedia(runId, createdAt, mediaItem, "attachments", attachmentIndex++));
            }
        }

        String htmlWithOssUrls = replaceCidReferences(parsed.htmlBody(), uploadedMedia, warnings);
        String htmlBodySanitized = htmlSanitizerService.sanitizeHtml(htmlWithOssUrls);
        String htmlRenderMode = htmlSanitizerService.htmlRenderMode(htmlWithOssUrls);
        Map<String, Object> payload = buildAgentBusLikePayload(
                runId,
                runLabel,
                externalMessageId,
                externalConversationId,
                originalMessageId,
                originalConversationId,
                parsed,
                htmlWithOssUrls,
                uploadedMedia);
        String payloadJson = objectMapper.writeValueAsString(payload);
        SourceMessageCaptureResult captureResult = sourceMessageCaptureService.capture(new CaptureSourceMessageCommand(
                hotelId,
                SOURCE_PROVIDER,
                SOURCE_CHANNEL,
                externalMessageId,
                externalConversationId,
                runId.toString(),
                "debug-eml-upload",
                parsed.sentAt(),
                parsed.sender(),
                parsed.subject(),
                parsed.textBody(),
                htmlWithOssUrls,
                payloadJson,
                SCHEMA_VERSION,
                uploadedMedia.stream().map(UploadedMedia::captureMedia).toList()));
        updateSourceCapturedRun(
                runId,
                captureResult.inboxId(),
                externalMessageId,
                externalConversationId,
                safeFileName,
                uploadedMedia.get(0).result().externalUrl(),
                sha256,
                payloadJson);

        SuperAgentOpenApiResult superAgentResult = superAgentOpenApiClient.invokeMailDebug(new SuperAgentMailDebugRequest(
                buildSuperAgentMessage(payloadJson),
                "debug-eml-" + runId,
                Map.of(
                        "source", "th-hotel-debug-eml-upload",
                        "debug_run_id", runId.toString(),
                        "source_message_id", captureResult.inboxId().toString(),
                        "hotel_id", hotelId)));
        JsonNode parsedJson = parseSuperAgentJson(superAgentResult.rawAnswer(), warnings);
        String parsedJsonText = parsedJson == null ? null : objectMapper.writeValueAsString(parsedJson);
        DebugEmlSuperAgentRunStatus status = DebugEmlSuperAgentRunStatus.SUPERAGENT_SUCCEEDED;
        runRepository.updateResult(new DebugEmlSuperAgentRunUpdate(
                runId,
                captureResult.inboxId(),
                externalMessageId,
                externalConversationId,
                safeFileName,
                uploadedMedia.get(0).result().externalUrl(),
                sha256,
                payloadJson,
                superAgentResult.sessionId(),
                superAgentResult.runId(),
                superAgentResult.profileId(),
                superAgentResult.profileVersionId(),
                superAgentResult.modelName(),
                superAgentResult.rawAnswer(),
                parsedJsonText,
                superAgentResult.inputTokens(),
                superAgentResult.outputTokens(),
                superAgentResult.totalTokens(),
                status.name(),
                null,
                nowUtc()));

        return new DebugEmlSuperAgentRunResult(
                runId.toString(),
                captureResult.inboxId().toString(),
                SOURCE_PROVIDER,
                externalMessageId,
                externalConversationId,
                uploadedMedia.get(0).result().externalUrl(),
                sha256,
                uploadedMedia.stream().map(UploadedMedia::result).toList(),
                htmlWithOssUrls,
                htmlBodySanitized,
                true,
                htmlRenderMode,
                payload,
                superAgentResult.sessionId(),
                superAgentResult.runId(),
                superAgentResult.rawAnswer(),
                parsedJson,
                List.copyOf(warnings),
                status.name());
    }

    /**
     * SourceMessage 已写入后先回填 Debug run，保证后续 SuperAgent 失败时仍可追踪入库消息和 OSS 原文。
     */
    private void updateSourceCapturedRun(
            Long runId,
            Long sourceMessageId,
            String externalMessageId,
            String externalConversationId,
            String safeFileName,
            String originalEmlOssUrl,
            String sha256,
            String payloadJson) {
        runRepository.updateResult(new DebugEmlSuperAgentRunUpdate(
                runId,
                sourceMessageId,
                externalMessageId,
                externalConversationId,
                safeFileName,
                originalEmlOssUrl,
                sha256,
                payloadJson,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                DebugEmlSuperAgentRunStatus.SOURCE_CAPTURED.name(),
                null,
                nowUtc()));
    }

    /**
     * 上传原始 .eml 文件。
     */
    private UploadedMedia uploadOriginalEml(Long runId, String safeFileName, byte[] emlBytes, LocalDateTime now) {
        String objectKey = objectKey(now, runId, "raw", safeFileName);
        ObjectStoragePutResult putResult = objectStorageService.putObject(new ObjectStoragePutRequest(
                objectKey,
                safeFileName,
                "message/rfc822",
                (long) emlBytes.length,
                emlBytes));
        return uploadedMedia(
                SourceMessageMediaType.ORIGINAL_EMAIL.code(),
                safeFileName,
                "message/rfc822",
                putResult.sizeBytes(),
                putResult.publicUrl(),
                "original-eml-" + runId,
                putResult.objectKey(),
                null);
    }

    /**
     * 上传解析出的内联图片或附件。
     */
    private UploadedMedia uploadParsedMedia(
            Long runId,
            LocalDateTime now,
            ParsedEmlMediaItem mediaItem,
            String folder,
            int index) {
        String fileName = safeFileName(mediaItem.fileName(), folder + "-" + index);
        String objectKey = objectKey(now, runId, folder, index + "-" + fileName);
        ObjectStoragePutResult putResult = objectStorageService.putObject(new ObjectStoragePutRequest(
                objectKey,
                fileName,
                mediaItem.contentType(),
                mediaItem.sizeBytes(),
                mediaItem.bytes()));
        String externalMediaId = SourceMessageMediaType.INLINE_IMAGE.code().equals(mediaItem.mediaType())
                ? "cid:" + mediaItem.contentId()
                : "attachment:" + index;
        return uploadedMedia(
                mediaItem.mediaType(),
                fileName,
                mediaItem.contentType(),
                putResult.sizeBytes(),
                putResult.publicUrl(),
                externalMediaId,
                putResult.objectKey(),
                mediaItem.contentId());
    }

    /**
     * 构造上传媒体结果和 SourceMessage 捕获媒体项。
     */
    private UploadedMedia uploadedMedia(
            String mediaType,
            String fileName,
            String contentType,
            Long sizeBytes,
            String externalUrl,
            String externalMediaId,
            String objectKey,
            String contentId) {
        DebugEmlUploadedMediaResult result = new DebugEmlUploadedMediaResult(
                mediaType,
                fileName,
                contentType,
                sizeBytes,
                externalUrl,
                externalMediaId,
                objectKey);
        CaptureSourceMessageMedia captureMedia = new CaptureSourceMessageMedia(
                mediaType,
                fileName,
                contentType,
                sizeBytes,
                externalUrl,
                externalMediaId);
        return new UploadedMedia(result, captureMedia, contentId);
    }

    /**
     * 替换 HTML 中的 cid: 引用为 OSS URL。
     */
    private String replaceCidReferences(String htmlBody, List<UploadedMedia> uploadedMedia, List<String> warnings) {
        if (htmlBody == null || htmlBody.isBlank()) {
            warnings.add("EML 未解析到 HTML 正文。");
            return htmlBody;
        }
        Map<String, String> cidUrlMap = new LinkedHashMap<>();
        for (UploadedMedia media : uploadedMedia) {
            if (media.contentId() == null || media.contentId().isBlank()) {
                continue;
            }
            cidUrlMap.put(normalizeCidValue(media.contentId()), media.result().externalUrl());
        }
        Matcher matcher = CID_REFERENCE_PATTERN.matcher(htmlBody);
        StringBuffer replaced = new StringBuffer();
        while (matcher.find()) {
            String externalUrl = cidUrlMap.get(normalizeCidReference(matcher.group()));
            if (externalUrl == null) {
                continue;
            }
            matcher.appendReplacement(replaced, Matcher.quoteReplacement(externalUrl));
        }
        matcher.appendTail(replaced);
        String result = replaced.toString();
        if (CID_REFERENCE_PATTERN.matcher(result).find()) {
            warnings.add("HTML 正文仍包含未匹配的 cid 图片引用。");
        }
        return result;
    }

    /**
     * 组装接近 AgentBus 邮件输入的 payload。
     */
    private Map<String, Object> buildAgentBusLikePayload(
            Long runId,
            String runLabel,
            String externalMessageId,
            String externalConversationId,
            String originalMessageId,
            String originalConversationId,
            ParsedEmlMessage parsed,
            String htmlWithOssUrls,
            List<UploadedMedia> uploadedMedia) {
        Map<String, Object> source = new LinkedHashMap<>();
        source.put("channel", SOURCE_CHANNEL);
        source.put("provider", SOURCE_PROVIDER);
        source.put("external_message_id", externalMessageId);
        source.put("external_conversation_id", externalConversationId);
        source.put("original_message_id", originalMessageId);
        source.put("original_conversation_id", originalConversationId);
        source.put("sender", parsed.sender());
        source.put("subject", parsed.subject());
        source.put("sent_at", parsed.sentAt() == null ? null : parsed.sentAt().toString());

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("content_type", contentType(parsed, htmlWithOssUrls));
        body.put("text", parsed.textBody());
        body.put("html", htmlWithOssUrls);

        List<Map<String, Object>> inlineImages = mediaPayload(uploadedMedia, SourceMessageMediaType.INLINE_IMAGE.code());
        List<Map<String, Object>> attachments = mediaPayload(uploadedMedia, SourceMessageMediaType.ATTACHMENT.code());

        Map<String, Object> replyPolicy = new LinkedHashMap<>();
        replyPolicy.put("mode", "debug_only");
        replyPolicy.put("final_only", true);

        Map<String, Object> debugContext = new LinkedHashMap<>();
        debugContext.put("debug_run_id", runId.toString());
        debugContext.put("run_label", runLabel);
        debugContext.put("original_eml_oss_url", uploadedMedia.get(0).result().externalUrl());

        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("schema_version", SCHEMA_VERSION);
        payload.put("source", source);
        payload.put("body", body);
        payload.put("inline_images", inlineImages);
        payload.put("attachments", attachments);
        payload.put("reply_policy", replyPolicy);
        payload.put("debug_context", debugContext);
        return payload;
    }

    /**
     * 构造媒体 payload 数组。
     */
    private List<Map<String, Object>> mediaPayload(List<UploadedMedia> uploadedMedia, String mediaType) {
        return uploadedMedia.stream()
                .filter(item -> mediaType.equals(item.result().mediaType()))
                .map(item -> {
                    Map<String, Object> media = new LinkedHashMap<>();
                    media.put("media_type", item.result().mediaType());
                    media.put("file_name", item.result().fileName());
                    media.put("content_type", item.result().contentType());
                    media.put("size_bytes", item.result().sizeBytes());
                    media.put("external_url", item.result().externalUrl());
                    media.put("external_media_id", item.result().externalMediaId());
                    media.put("object_key", item.result().objectKey());
                    if (item.contentId() != null) {
                        media.put("content_id", item.contentId());
                    }
                    return media;
                })
                .toList();
    }

    /**
     * 根据正文存在情况返回 body content type。
     */
    private String contentType(ParsedEmlMessage parsed, String htmlWithOssUrls) {
        boolean hasText = parsed.textBody() != null && !parsed.textBody().isBlank();
        boolean hasHtml = htmlWithOssUrls != null && !htmlWithOssUrls.isBlank();
        if (hasText && hasHtml) {
            return "MIXED";
        }
        return hasHtml ? "HTML" : "TEXT";
    }

    /**
     * 构造发送给 SuperAgent 的消息文本。
     */
    private String buildSuperAgentMessage(String payloadJson) {
        return "请基于以下 Debug 邮件 JSON 输出结构化任务抽取结果，只返回 JSON，不要创建订单或任务：\n" + payloadJson;
    }

    /**
     * 尝试把 SuperAgent 最终回答解析为 JSON，失败时返回 null 并记录 warning。
     */
    private JsonNode parseSuperAgentJson(String rawAnswer, List<String> warnings) {
        if (rawAnswer == null || rawAnswer.isBlank()) {
            warnings.add("SuperAgent 最终回答为空。");
            return null;
        }
        try {
            return objectMapper.readTree(rawAnswer);
        } catch (Exception exception) {
            warnings.add("SuperAgent 最终回答不是合法 JSON，已保留 raw answer。");
            return null;
        }
    }

    /**
     * 标记 Debug 运行失败。
     */
    private void markFailed(
            Long runId,
            String safeSummary,
            DebugEmlSuperAgentRunStatus status,
            LocalDateTime updatedAt) {
        if (runId == null) {
            return;
        }
        runRepository.updateStatus(new DebugEmlSuperAgentRunStatusUpdate(
                runId,
                status.name(),
                truncate(safeSummary, 512),
                updatedAt));
    }

    /**
     * 根据异常选择 Debug run 状态。
     */
    private DebugEmlSuperAgentRunStatus statusForException(DebugEmlSuperAgentException exception) {
        return exception.getStatus() == HttpStatus.BAD_GATEWAY
                ? DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED
                : DebugEmlSuperAgentRunStatus.FAILED;
    }

    /**
     * 校验调试访问口令。
     */
    private void validateAccessKey(String accessKey) {
        if (properties.getAccessKey() == null || properties.getAccessKey().isBlank()
                || accessKey == null || !properties.getAccessKey().equals(accessKey)) {
            throw new DebugEmlSuperAgentException(HttpStatus.UNAUTHORIZED, "DEBUG_UPLOAD_KEY_INVALID", "Debug 上传访问口令缺失或错误。");
        }
    }

    /**
     * 校验上传文件基本属性。
     */
    private void validateFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new DebugEmlSuperAgentException(HttpStatus.BAD_REQUEST, "EML_FILE_REQUIRED", "请上传 .eml 邮件文件。");
        }
        if (file.getSize() > properties.getMaxFileBytes()) {
            throw new DebugEmlSuperAgentException(HttpStatus.BAD_REQUEST, "EML_FILE_TOO_LARGE", "上传的 .eml 文件超过大小限制。");
        }
    }

    /**
     * 读取上传文件字节。
     */
    private byte[] readFileBytes(MultipartFile file) {
        try {
            return file.getBytes();
        } catch (Exception exception) {
            throw new DebugEmlSuperAgentException(
                    HttpStatus.BAD_REQUEST,
                    "EML_FILE_READ_FAILED",
                    "读取上传 .eml 文件失败。",
                    exception);
        }
    }

    /**
     * 生成 OSS 对象路径。
     */
    private String objectKey(LocalDateTime now, Long runId, String folder, String fileName) {
        String prefix = ossProperties.getDebugEmlPrefix();
        String normalizedPrefix = prefix == null || prefix.isBlank() ? "debug/eml/" : prefix;
        if (!normalizedPrefix.endsWith("/")) {
            normalizedPrefix = normalizedPrefix + "/";
        }
        return normalizedPrefix
                + DATE_FOLDER_FORMATTER.format(now.toLocalDate())
                + "/" + runId
                + "/" + folder
                + "/" + fileName;
    }

    /**
     * 生成 Debug 专属外部消息 ID，避免重复上传同一封 EML 时命中 SourceMessage 幂等复用。
     */
    private String debugExternalMessageId(Long runId, String sha256) {
        return "debug-eml-run-" + runId + "-" + sha256.substring(0, 12);
    }

    /**
     * 规范化 HTML 中匹配到的 cid: 引用。
     */
    private String normalizeCidReference(String cidReference) {
        if (cidReference == null || cidReference.length() <= 4) {
            return "";
        }
        return normalizeCidValue(cidReference.substring(4));
    }

    /**
     * 规范化 Content-ID，兼容尖括号、URL 编码和大小写差异。
     */
    private String normalizeCidValue(String value) {
        if (value == null) {
            return "";
        }
        String normalized = urlDecode(value.trim());
        if (normalized.startsWith("<") && normalized.endsWith(">") && normalized.length() > 2) {
            normalized = normalized.substring(1, normalized.length() - 1);
        }
        return normalized.trim().toLowerCase(Locale.ROOT);
    }

    /**
     * URL 解码 cid 内容；保留加号字面值，解码失败时保留原始值继续匹配。
     */
    private String urlDecode(String value) {
        try {
            return URLDecoder.decode(value.replace("+", "%2B"), StandardCharsets.UTF_8);
        } catch (Exception exception) {
            return value;
        }
    }

    /**
     * 文件名安全清洗，避免路径穿越和日志污染。
     */
    private String safeFileName(String fileName, String fallback) {
        String candidate = fileName == null || fileName.isBlank() ? fallback : fileName;
        int slashIndex = Math.max(candidate.lastIndexOf('/'), candidate.lastIndexOf('\\'));
        if (slashIndex >= 0) {
            candidate = candidate.substring(slashIndex + 1);
        }
        String sanitized = candidate.replaceAll("[^A-Za-z0-9._-]", "_");
        if (sanitized.isBlank()) {
            sanitized = fallback;
        }
        return sanitized.length() > 160 ? sanitized.substring(sanitized.length() - 160) : sanitized;
    }

    /**
     * 计算 SHA-256。
     */
    private String sha256(byte[] bytes) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(digest.digest(bytes));
        } catch (NoSuchAlgorithmException exception) {
            throw new IllegalStateException("当前 Java 运行时不支持 SHA-256", exception);
        }
    }

    /**
     * 必填文本校验。
     */
    private String requireText(String value, String fieldName) {
        String trimmed = trimToNull(value);
        if (trimmed == null) {
            throw new DebugEmlSuperAgentException(HttpStatus.BAD_REQUEST, "REQUEST_FIELD_REQUIRED", fieldName + " 不能为空。");
        }
        return trimmed;
    }

    /**
     * 返回第一个非空文本。
     */
    private String firstNonBlank(String first, String second) {
        String normalizedFirst = trimToNull(first);
        return normalizedFirst == null ? second : normalizedFirst;
    }

    /**
     * 空白字符串转 null。
     */
    private String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    /**
     * 截断安全摘要，避免超过数据库限制。
     */
    private String truncate(String value, int maxLength) {
        if (value == null || value.length() <= maxLength) {
            return value;
        }
        return value.substring(0, maxLength);
    }

    /**
     * 当前 UTC 时间。
     */
    private LocalDateTime nowUtc() {
        return LocalDateTime.now(ZoneOffset.UTC);
    }

    /**
     * 上传媒体内部组合对象。
     */
    private record UploadedMedia(
            DebugEmlUploadedMediaResult result,
            CaptureSourceMessageMedia captureMedia,
            String contentId
    ) {
    }
}
