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.common.result.SuperAgentOpenApiTraceEvent;
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.common.enums.SourceMessageOnlyResultCode;
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.hotel.service.HotelContextService;
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
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.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
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.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClientResponseException;

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

    private static final Logger log = LoggerFactory.getLogger(DebugEmlSuperAgentRunServiceImpl.class);
    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 static final Pattern JSON_SECRET_PATTERN = Pattern.compile(
            "(?i)(\"(?:api[_-]?key|token|secret|password|cookie|authorization)\"\\s*:\\s*\")[^\"]*(\")");
    private static final Pattern AUTHORIZATION_BEARER_SECRET_PATTERN = Pattern.compile(
            "(?i)(authorization\\s*[:=]\\s*bearer\\s+)[^\\s,;]+");
    private static final Pattern AUTHORIZATION_SECRET_PATTERN = Pattern.compile(
            "(?i)(authorization\\s*[:=]\\s*)(?!bearer\\s)[^\\s,;]+");
    private static final Pattern COOKIE_SECRET_PATTERN = Pattern.compile(
            "(?i)(cookie\\s*[:=]\\s*)[^\\s,;]+");
    private static final Pattern TEXT_SECRET_PATTERN = Pattern.compile(
            "(?i)((?:api[_-]?key|token|secret|password)\\s*[:=]\\s*)[^\\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;
    private final HotelContextService hotelContextService;

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

    /**
     * 校验 Debug EML 上传访问口令。供 Controller 在流式响应开始前复用，避免错误被写成 SSE 事件后丢失 HTTP 状态。
     */
    @Override
    public void validateUploadAccessKey(String accessKey) {
        validateAccessKey(accessKey);
    }

    /**
     * 处理单封 Debug EML 上传；第一版只展示 SuperAgent 结果，不创建订单或任务。
     */
    @Override
    public DebugEmlSuperAgentRunResult uploadAndRun(
            String accessKey,
            MultipartFile file,
            String hotelId,
            String runLabel) {
        validateAccessKey(accessKey);
        String normalizedHotelId = normalizeHotelId(hotelId);
        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, null);
        } 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) {
            String safeSummary = superAgentFailureSummary(exception);
            logSuperAgentOpenApiFailure(runId, safeSummary, exception);
            markFailed(runId, safeSummary, 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);
        }
    }

    /**
     * 处理单封 Debug EML 上传，并实时输出安全的调试 SSE 事件。
     */
    @Override
    public void uploadAndRunStream(
            String accessKey,
            MultipartFile file,
            String hotelId,
            String runLabel,
            OutputStream outputStream) {
        OutputStreamDebugEmlStreamSink streamSink = new OutputStreamDebugEmlStreamSink(outputStream, objectMapper);
        Long runId = null;
        try {
            validateAccessKey(accessKey);
            String normalizedHotelId = normalizeHotelId(hotelId);
            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();
            runId = runRepository.insert(new DebugEmlSuperAgentRunDraft(
                    normalizedHotelId,
                    trimToNull(runLabel),
                    DebugEmlSuperAgentRunStatus.CREATED.name(),
                    now));
            DebugEmlSuperAgentRunResult result = doUploadAndRun(
                    runId,
                    normalizedHotelId,
                    trimToNull(runLabel),
                    safeFileName,
                    emlBytes,
                    now,
                    streamSink);
            streamSink.result(result);
            streamSink.done();
        } catch (DebugEmlSuperAgentException exception) {
            if (runId != null) {
                markFailed(runId, exception.getMessage(), statusForException(exception), nowUtc());
            }
            streamSink.error(runId, exception.getErrorCode(), exception.getMessage(), statusForException(exception).name());
        } catch (EmlMessageParseException exception) {
            if (runId != null) {
                markFailed(runId, "EML 邮件解析失败。", DebugEmlSuperAgentRunStatus.FAILED, nowUtc());
            }
            streamSink.error(runId, "EML_PARSE_FAILED", "EML 邮件解析失败。", DebugEmlSuperAgentRunStatus.FAILED.name());
        } catch (ObjectStorageException exception) {
            if (runId != null) {
                markFailed(runId, "OSS 上传失败。", DebugEmlSuperAgentRunStatus.FAILED, nowUtc());
            }
            streamSink.error(runId, "OSS_UPLOAD_FAILED", "OSS 上传失败。", DebugEmlSuperAgentRunStatus.FAILED.name());
        } catch (SuperAgentOpenApiException exception) {
            if (runId != null) {
                String safeSummary = superAgentFailureSummary(exception);
                logSuperAgentOpenApiFailure(runId, safeSummary, exception);
                markFailed(runId, safeSummary, DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED, nowUtc());
            }
            streamSink.error(
                    runId,
                    "SUPERAGENT_OPEN_API_FAILED",
                    "SuperAgent 调用失败。",
                    DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED.name());
        } catch (Exception exception) {
            if (runId != null) {
                markFailed(runId, "Debug EML 上传处理失败。", DebugEmlSuperAgentRunStatus.FAILED, nowUtc());
            }
            streamSink.error(
                    runId,
                    "DEBUG_EML_RUN_FAILED",
                    "Debug EML 上传处理失败。",
                    DebugEmlSuperAgentRunStatus.FAILED.name());
        }
    }

    /**
     * 解析 Debug 上传酒店上下文。第一版可不传 hotel_id，由单酒店系统上下文兜底。
     */
    private String normalizeHotelId(String hotelId) {
        try {
            return hotelContextService.resolveCurrentHotelId(hotelId);
        } catch (HotelContextException exception) {
            throw new DebugEmlSuperAgentException(
                    exception.getStatus(),
                    exception.getErrorCode(),
                    exception.getMessage(),
                    exception);
        }
    }

    /**
     * 生成 SuperAgent 失败安全摘要，只记录错误类型，不保存响应 body、API Key、正文或附件 URL。
     */
    private String superAgentFailureSummary(SuperAgentOpenApiException exception) {
        Throwable rootCause = rootCause(exception);
        if (rootCause instanceof RestClientResponseException responseException) {
            return "SuperAgent Open API HTTP 调用失败，HTTP 状态：" + responseException.getStatusCode().value() + "。";
        }
        if (hasCause(exception, SocketTimeoutException.class)) {
            return "SuperAgent Open API 调用超时。";
        }
        if (hasCause(exception, ResourceAccessException.class)) {
            return "SuperAgent Open API 网络连接失败。";
        }
        if (rootCause instanceof JsonProcessingException) {
            return "SuperAgent Open API 响应 JSON 解析失败。";
        }
        String message = trimToNull(exception.getMessage());
        if (message != null && !"SuperAgent Open API 调用失败。".equals(message)) {
            return safeErrorSummary(message);
        }
        return "SuperAgent Open API 调用失败。";
    }

    /**
     * 记录 SuperAgent Debug 调用失败的安全诊断日志。只写异常类型和脱敏消息，不记录 API Key、Cookie、邮件正文或响应体。
     */
    private void logSuperAgentOpenApiFailure(
            Long runId,
            String safeSummary,
            SuperAgentOpenApiException exception) {
        log.warn(
                "Debug EML SuperAgent Open API failed. debug_run_id={}, safe_error_summary={}, exception_chain={}",
                runId,
                safeSummary,
                safeExceptionChain(exception));
    }

    /**
     * 生成异常链摘要，帮助区分 timeout、connection reset、EOF 等网络/流读取问题。
     */
    private String safeExceptionChain(Throwable throwable) {
        List<String> chain = new ArrayList<>();
        Throwable current = throwable;
        int depth = 0;
        while (current != null && depth < 8) {
            String message = trimToNull(current.getMessage());
            String item = current.getClass().getSimpleName();
            if (message != null) {
                item += ": " + sanitizeLogMessage(message);
            }
            chain.add(item);
            current = current.getCause();
            depth++;
        }
        return String.join(" <- ", chain);
    }

    /**
     * 日志消息脱敏并截断，避免外部异常把 Secret 或过长内容写入日志。
     */
    private String sanitizeLogMessage(String message) {
        String sanitized = JSON_SECRET_PATTERN.matcher(message).replaceAll("$1[REDACTED]$2");
        sanitized = AUTHORIZATION_BEARER_SECRET_PATTERN.matcher(sanitized).replaceAll("$1[REDACTED]");
        sanitized = AUTHORIZATION_SECRET_PATTERN.matcher(sanitized).replaceAll("$1[REDACTED]");
        sanitized = COOKIE_SECRET_PATTERN.matcher(sanitized).replaceAll("$1[REDACTED]");
        sanitized = TEXT_SECRET_PATTERN.matcher(sanitized).replaceAll("$1[REDACTED]");
        return safeErrorSummary(sanitized);
    }

    /**
     * 取最底层异常，便于判断 HTTP、网络和解析失败类型。
     */
    private Throwable rootCause(Throwable throwable) {
        Throwable current = throwable;
        while (current.getCause() != null) {
            current = current.getCause();
        }
        return current;
    }

    /**
     * 判断异常链中是否包含指定类型。
     */
    private boolean hasCause(Throwable throwable, Class<? extends Throwable> causeType) {
        Throwable current = throwable;
        while (current != null) {
            if (causeType.isInstance(current)) {
                return true;
            }
            current = current.getCause();
        }
        return false;
    }

    /**
     * 限制错误摘要长度，避免外部异常消息过长进入调试表。
     */
    private String safeErrorSummary(String message) {
        String value = message.trim();
        if (value.length() <= 512) {
            return value;
        }
        return value.substring(0, 512);
    }

    /**
     * 执行已创建 runId 的主流程。
     */
    private DebugEmlSuperAgentRunResult doUploadAndRun(
            Long runId,
            String hotelId,
            String runLabel,
            String safeFileName,
            byte[] emlBytes,
            LocalDateTime createdAt,
            DebugEmlSuperAgentStreamSink streamSink) throws Exception {
        String sha256 = sha256(emlBytes);
        markStage(
                runId,
                DebugEmlSuperAgentRunStatus.PARSING_EML,
                "阶段：解析 EML 邮件，文件名：" + safeFileName + "，大小：" + emlBytes.length + " bytes。",
                streamSink);
        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<>();

        markStage(
                runId,
                DebugEmlSuperAgentRunStatus.UPLOADING_ORIGINAL_EML,
                "阶段：上传原始 EML 到 OSS，文件名：" + safeFileName + "，大小：" + emlBytes.length + " bytes。",
                streamSink);
        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())) {
                markStage(
                        runId,
                        DebugEmlSuperAgentRunStatus.UPLOADING_MEDIA,
                        mediaUploadStageSummary(mediaItem, "inline", inlineIndex),
                        streamSink);
                uploadedMedia.add(uploadParsedMedia(runId, createdAt, mediaItem, "inline", inlineIndex++));
            } else {
                markStage(
                        runId,
                        DebugEmlSuperAgentRunStatus.UPLOADING_MEDIA,
                        mediaUploadStageSummary(mediaItem, "attachments", attachmentIndex),
                        streamSink);
                uploadedMedia.add(uploadParsedMedia(runId, createdAt, mediaItem, "attachments", attachmentIndex++));
            }
        }

        markStage(
                runId,
                DebugEmlSuperAgentRunStatus.BUILDING_SOURCE_MESSAGE,
                "阶段：构造 SourceMessage payload。",
                streamSink);
        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);
        markStage(
                runId,
                DebugEmlSuperAgentRunStatus.CAPTURING_SOURCE_MESSAGE,
                "阶段：写入 SourceMessage Inbox。",
                streamSink);
        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);

        markStage(
                runId,
                DebugEmlSuperAgentRunStatus.CALLING_SUPERAGENT,
                "阶段：调用 SuperAgent Open API，source_message_id=" + captureResult.inboxId() + "。",
                streamSink);
        SuperAgentMailDebugRequest superAgentRequest = 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));
        List<SuperAgentOpenApiTraceEvent> streamedTraceEvents = new ArrayList<>();
        Consumer<SuperAgentOpenApiTraceEvent> traceConsumer = traceEvent -> {
            streamedTraceEvents.add(traceEvent);
            if (streamSink != null) {
                streamSink.trace(traceEvent);
            }
        };
        SuperAgentOpenApiResult superAgentResult = invokeSuperAgentWithHeartbeat(
                runId,
                superAgentRequest,
                traceConsumer,
                streamSink);
        List<SuperAgentOpenApiTraceEvent> traceEvents = streamedTraceEvents.isEmpty()
                ? superAgentResult.traceEvents()
                : List.copyOf(streamedTraceEvents);
        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,
                traceEvents,
                List.copyOf(warnings),
                status.name());
    }

    /**
     * 调用 SuperAgent 期间维持 Debug SSE 心跳，避免无 trace 输出时浏览器或反向代理认为连接空闲。
     */
    private SuperAgentOpenApiResult invokeSuperAgentWithHeartbeat(
            Long runId,
            SuperAgentMailDebugRequest request,
            Consumer<SuperAgentOpenApiTraceEvent> traceConsumer,
            DebugEmlSuperAgentStreamSink streamSink) {
        if (streamSink == null) {
            return superAgentOpenApiClient.invokeMailDebug(request);
        }
        ScheduledExecutorService heartbeatExecutor = null;
        ScheduledFuture<?> heartbeatTask = null;
        try {
            Duration interval = properties.getSseHeartbeatInterval();
            if (interval != null && !interval.isZero() && !interval.isNegative()) {
                heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(heartbeatThreadFactory(runId));
                long intervalMillis = Math.max(1L, interval.toMillis());
                heartbeatTask = heartbeatExecutor.scheduleAtFixedRate(
                        () -> streamSink.heartbeat(runId),
                        intervalMillis,
                        intervalMillis,
                        TimeUnit.MILLISECONDS);
            }
            return superAgentOpenApiClient.invokeMailDebug(request, traceConsumer);
        } finally {
            if (heartbeatTask != null) {
                heartbeatTask.cancel(false);
            }
            if (heartbeatExecutor != null) {
                heartbeatExecutor.shutdownNow();
            }
        }
    }

    /**
     * 创建 Debug SSE 心跳线程；使用 daemon 线程，避免调试请求异常退出时阻塞 JVM 关闭。
     */
    private ThreadFactory heartbeatThreadFactory(Long runId) {
        return runnable -> {
            Thread thread = new Thread(runnable, "debug-eml-sse-heartbeat-" + runId);
            thread.setDaemon(true);
            return thread;
        };
    }

    /**
     * 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;
        }
        JsonNode sourceMessageOnlyResult = parseSourceMessageOnlyResult(rawAnswer);
        if (sourceMessageOnlyResult != null) {
            return sourceMessageOnlyResult;
        }
        try {
            return objectMapper.readTree(rawAnswer);
        } catch (Exception exception) {
            warnings.add("SuperAgent 最终回答不是合法 JSON，已保留 raw answer。");
            return null;
        }
    }

    /**
     * 识别 Debug 链路中的 S000/S999 入口结果，避免把可识别文本误报为 JSON 解析失败。
     */
    private JsonNode parseSourceMessageOnlyResult(String rawAnswer) {
        String trimmedAnswer = trimToNull(rawAnswer);
        if (trimmedAnswer == null) {
            return null;
        }
        int separatorIndex = trimmedAnswer.indexOf(',');
        String code = separatorIndex < 0 ? trimmedAnswer : trimmedAnswer.substring(0, separatorIndex).trim();
        SourceMessageOnlyResultCode resultCode = SourceMessageOnlyResultCode.fromCode(code).orElse(null);
        if (resultCode == null || separatorIndex < 0) {
            return null;
        }
        String sourceMessageId = trimToNull(trimmedAnswer.substring(separatorIndex + 1));
        if (sourceMessageId == null) {
            return null;
        }
        ObjectNode result = objectMapper.createObjectNode();
        result.put("entry_result_code", resultCode.code());
        result.put("entry_result_source_message_id", sourceMessageId);
        result.put("entry_result_meaning", resultCode.meaning());
        result.put("entry_result_description", resultCode.description());
        return result;
    }

    /**
     * 标记 Debug 运行中的当前阶段。这里复用安全摘要字段保存短文本面包屑，便于测试环境定位卡点。
     */
    private void markStage(
            Long runId,
            DebugEmlSuperAgentRunStatus status,
            String safeSummary) {
        markStage(runId, status, safeSummary, null);
    }

    /**
     * 标记 Debug 运行中的当前阶段，并在流式调试入口实时输出阶段事件。
     */
    private void markStage(
            Long runId,
            DebugEmlSuperAgentRunStatus status,
            String safeSummary,
            DebugEmlSuperAgentStreamSink streamSink) {
        if (runId == null) {
            return;
        }
        String truncatedSummary = truncate(safeSummary, 512);
        runRepository.updateStatus(new DebugEmlSuperAgentRunStatusUpdate(
                runId,
                status.name(),
                truncatedSummary,
                nowUtc()));
        if (streamSink != null) {
            streamSink.stage(runId, status, truncatedSummary);
        }
    }

    /**
     * 标记 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));
    }

    /**
     * 构造媒体上传阶段摘要，只包含安全元数据，不包含正文、公开 URL 或二进制内容。
     */
    private String mediaUploadStageSummary(ParsedEmlMediaItem mediaItem, String folder, int index) {
        String fileName = safeFileName(mediaItem.fileName(), folder + "-" + index);
        return "阶段：上传邮件媒体到 OSS，目录：" + folder
                + "，序号：" + index
                + "，类型：" + mediaItem.mediaType()
                + "，文件名：" + fileName
                + "，大小：" + mediaItem.sizeBytes() + " bytes。";
    }

    /**
     * 根据异常选择 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);
    }

    /**
     * Debug EML 流式事件输出端口，避免主流程依赖 HTTP 细节。
     */
    private interface DebugEmlSuperAgentStreamSink {

        /**
         * 输出本系统处理阶段。
         */
        void stage(Long runId, DebugEmlSuperAgentRunStatus status, String safeSummary);

        /**
         * 输出 SuperAgent 公开 Trace 事件。
         */
        void trace(SuperAgentOpenApiTraceEvent traceEvent);

        /**
         * 输出安全心跳事件，维持 Debug SSE 长连接。
         */
        void heartbeat(Long runId);

        /**
         * 输出最终 Debug 结果。
         */
        void result(DebugEmlSuperAgentRunResult result);

        /**
         * 输出安全错误事件。
         */
        void error(Long runId, String errorCode, String message, String status);

        /**
         * 输出流结束事件。
         */
        void done();
    }

    /**
     * 将 Debug EML 事件写成 text/event-stream。
     */
    private static final class OutputStreamDebugEmlStreamSink implements DebugEmlSuperAgentStreamSink {

        private final OutputStream outputStream;
        private final ObjectMapper objectMapper;
        private boolean active = true;

        private OutputStreamDebugEmlStreamSink(OutputStream outputStream, ObjectMapper objectMapper) {
            this.outputStream = outputStream;
            this.objectMapper = objectMapper;
        }

        @Override
        public void stage(Long runId, DebugEmlSuperAgentRunStatus status, String safeSummary) {
            Map<String, Object> data = new LinkedHashMap<>();
            data.put("debug_run_id", runId == null ? null : runId.toString());
            data.put("status", status.name());
            data.put("safe_summary", safeSummary);
            send("debug_stage", data);
        }

        @Override
        public void trace(SuperAgentOpenApiTraceEvent traceEvent) {
            send("superagent_trace", traceEvent);
        }

        @Override
        public void heartbeat(Long runId) {
            Map<String, Object> data = new LinkedHashMap<>();
            data.put("debug_run_id", runId == null ? null : runId.toString());
            data.put("status", DebugEmlSuperAgentRunStatus.CALLING_SUPERAGENT.name());
            send("debug_heartbeat", data);
        }

        @Override
        public void result(DebugEmlSuperAgentRunResult result) {
            send("superagent_result", result);
        }

        @Override
        public void error(Long runId, String errorCode, String message, String status) {
            Map<String, Object> data = new LinkedHashMap<>();
            data.put("debug_run_id", runId == null ? null : runId.toString());
            data.put("error_code", errorCode);
            data.put("message", message);
            data.put("status", status);
            send("debug_error", data);
            done();
        }

        @Override
        public void done() {
            send("done", Map.of("status", "done"));
        }

        /**
         * 写出一个 SSE 事件块并立即 flush。写失败通常表示客户端断开，只关闭流输出，不影响后续业务落库。
         */
        private synchronized void send(String eventName, Object data) {
            if (!active) {
                return;
            }
            try {
                String eventBlock = "event: " + eventName
                        + "\n"
                        + "data: " + objectMapper.writeValueAsString(data)
                        + "\n\n";
                outputStream.write(eventBlock.getBytes(StandardCharsets.UTF_8));
                outputStream.flush();
            } catch (IOException exception) {
                active = false;
            }
        }
    }

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