package cn.nianxx.thhotel.workflows.reservation.service.impl;

import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageInboxSnapshot;
import cn.nianxx.thhotel.platform.message.repository.SourceMessageInboxRepository;
import cn.nianxx.thhotel.platform.common.enums.SourceMessageOnlyResultCode;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskDraft;
import cn.nianxx.thhotel.workflows.reservation.common.enums.AiResultType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationAiRouteDefinition;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationAiSystemProcessCategory;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationBusinessKeySource;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderKeyType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderVisibility;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationReviewStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationSystemTaskType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskCardType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskStatus;
import cn.nianxx.thhotel.workflows.reservation.common.result.SuperAgentTaskResultItemResponse;
import cn.nianxx.thhotel.workflows.reservation.common.result.SuperAgentTaskResultResponse;
import cn.nianxx.thhotel.workflows.reservation.common.result.SuperAgentTaskResultWarning;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
import cn.nianxx.thhotel.workflows.reservation.service.ReservationAiTaskIntakeService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Reservation AI 任务结果接收实现。负责 CP1-3：AI 过渡层、订单、任务和任务卡创建。
 */
@Service
public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskIntakeService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ReservationAiTaskIntakeServiceImpl.class);

    private static final String FIELD_CONTRACT_VERSION = "20260711-p0";
    private static final String BATCH_KEY_PREFIX = "superagent-task-result-batch:v1";
    private static final String ITEM_KEY_PREFIX = "superagent-task-result-item:v1";
    private static final String SOURCE_MESSAGE_ONLY_RESULT_TYPE = "source_message_only";
    private static final String SOURCE_MESSAGE_ONLY_CATALOG_CODE = "ENTRY_RESULT";
    private static final String SOURCE_MESSAGE_ONLY_SKILL_ID = "superagent_entry_router";
    private static final String V3_CATALOG_CODE = "M002V3";
    private static final String V3_SKILL_ID = "booking-desk-event";
    private static final String ADAPTER_CONTRACT_ERROR_CODE = "adapter_contract_error";
    private static final String DEFAULT_SOURCE_PROVIDER = "AGENTBUS";
    private static final String DEFAULT_SOURCE_CHANNEL = "EMAIL";
    private static final int LENGTH_32 = 32;
    private static final int LENGTH_64 = 64;
    private static final int LENGTH_128 = 128;
    private static final int LENGTH_256 = 256;
    private static final int LENGTH_512 = 512;
    private static final int MAX_QUEUE_ORDER_RETRY = 5;
    private static final List<String> V3_SOURCE_MESSAGE_KEYS = List.of(
            "source_message_id",
            "subject",
            "from",
            "cc",
            "received_at",
            "source_channel");
    private static final List<String> V3_MAIN_OUTCOME_KEYS = List.of(
            "source_message",
            "route_code",
            "handler_type",
            "result_type",
            "current_or_history",
            "agent_assessment",
            "notification",
            "manual_review");
    private static final List<String> V3_AGENT_ASSESSMENT_KEYS = List.of(
            "status",
            "reason_code",
            "automation_action");
    private static final List<String> V3_NOTIFICATION_KEYS = List.of(
            "required",
            "notification_type",
            "show_source_message",
            "requires_user_decision",
            "visible_message");
    private static final List<String> V3_REVIEW_KEYS = List.of(
            "reason_code",
            "visible_reason",
            "review_record_type",
            "missing_fields",
            "blocking_points",
            "conflicting_points",
            "suggested_human_actions",
            "evidence_to_check",
            "known_fields");
    private static final List<String> V3_BUSINESS_ROOT_KEYS = List.of(
            "source_message",
            "message_events",
            "case_candidates",
            "extraction_warnings",
            "unhandled_current_intents");
    private static final List<String> V3_EVENT_REQUIRED_KEYS = List.of(
            "event_type",
            "event_role",
            "current_or_history",
            "source_event_index",
            "case_keys",
            "relevant_message_excerpt",
            "attachments",
            "file_references",
            "context_used",
            "extracted_fields",
            "manual_review");
    private static final List<String> V3_CASE_KEY_KEYS = List.of(
            "group_code",
            "confirmation_number",
            "reservation_number",
            "block_code");
    private static final List<String> V3_REVIEW_ARRAY_KEYS = List.of(
            "missing_fields",
            "blocking_points",
            "conflicting_points",
            "suggested_human_actions",
            "evidence_to_check");
    private static final Set<String> V3_ACTIVE_EVENT_TYPES = Set.of(
            "New Booking",
            "Update Booking / Amendment",
            "Cancel Booking",
            "Cancel Allotment",
            "Voucher Received",
            "Payment Evidence",
            "Rooming List",
            "AMEND GROUP CODE",
            "Invoice Generation",
            "Invoice Received",
            "Payment Notice",
            "Trace",
            "Manual RateCode",
            "TA RECORDER");

    private final ObjectMapper objectMapper;
    private final SourceMessageInboxRepository sourceMessageInboxRepository;
    private final ReservationAiWorkflowRepository workflowRepository;
    private final boolean allowLegacyInternalSourceMessageId;

    /**
     * 注入 JSON 解析器、SourceMessage 查询边界、Reservation 持久化边界和本地旧夹具兼容开关。
     */
    public ReservationAiTaskIntakeServiceImpl(
            ObjectMapper objectMapper,
            SourceMessageInboxRepository sourceMessageInboxRepository,
            ReservationAiWorkflowRepository workflowRepository,
            @Value("${superagent.task-result.allow-legacy-internal-source-message-id:false}")
            boolean allowLegacyInternalSourceMessageId) {
        this.objectMapper = objectMapper;
        this.sourceMessageInboxRepository = sourceMessageInboxRepository;
        this.workflowRepository = workflowRepository;
        this.allowLegacyInternalSourceMessageId = allowLegacyInternalSourceMessageId;
    }

    /**
     * 接收已通过 HMAC 鉴权的 SuperAgent 任务结果，创建 AI 过渡记录和最小任务模型。
     */
    @Override
    @Transactional
    public SuperAgentTaskResultResponse accept(String rawBody, String clientId, String requestId) {
        return accept(rawBody, clientId, requestId, null);
    }

    /**
     * 接收已通过 HMAC 鉴权的 SuperAgent 任务结果。先处理旧文本结果，再分流 V3 JSON 和 V2 JSON。
     */
    @Override
    @Transactional
    public SuperAgentTaskResultResponse accept(String rawBody, String clientId, String requestId, String defaultHotelId) {
        String requestBody = rawBody == null ? "" : rawBody;
        SourceMessageOnlyEntryResult sourceMessageOnlyEntryResult = parseSourceMessageOnlyEntryResult(requestBody);
        if (sourceMessageOnlyEntryResult != null) {
            return acceptSourceMessageOnlyEntryResult(
                    sourceMessageOnlyEntryResult,
                    clientId,
                    requestId,
                    defaultHotelId,
                    requestBody);
        }
        JsonNode root = parseJson(rawBody);
        if (isV3SourceMessageIdentityMissing(root)) {
            throw infrastructureInputError();
        }
        if (isMissingSourceMessageInfrastructureInputError(root)) {
            throw infrastructureInputError();
        }
        if (isInfrastructureInputError(root)) {
            throw error(HttpStatus.BAD_REQUEST, "INFRASTRUCTURE_INPUT_ERROR", "SuperAgent 返回基础设施输入错误。");
        }
        if (isV3SourceMessageNotification(root)) {
            return acceptV3SourceMessageNotification(root, clientId, requestId, defaultHotelId, requestBody);
        }
        if (isV3BusinessRoot(root)) {
            return acceptV3BusinessRoot(root, clientId, requestId, defaultHotelId, requestBody);
        }
        ResolvedSourceMessage resolvedSourceMessage = resolveSourceMessage(root, defaultHotelId);
        SourceMessageInboxSnapshot sourceMessage = resolvedSourceMessage.snapshot();
        Long sourceMessageId = sourceMessage.id();
        String responseSourceMessageId = resolvedSourceMessage.responseSourceMessageId();
        JsonNode taskResults = root.path("ai_task_results");
        if (!taskResults.isArray() || taskResults.isEmpty()) {
            throw error(HttpStatus.BAD_REQUEST, "TASK_RESULTS_EMPTY", "ai_task_results 不能为空。");
        }
        V3EventContractIssue legacyIssue = inspectV2TaskResultsContractIssue(taskResults);
        if (legacyIssue != null) {
            throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", legacyIssue.message());
        }

        String hotelId = sourceMessage.hotelId();
        String requestPayloadSha256 = sha256(rawBody == null ? "" : rawBody);
        String batchIdempotencyKey = sha256(BATCH_KEY_PREFIX + "|" + sourceMessageId + "|" + requestPayloadSha256);

        ReservationAiBatchSnapshot existingBatch = workflowRepository
                .findBatchBySourceMessageId(hotelId, sourceMessageId)
                .orElse(null);
        if (existingBatch != null) {
            return handleExistingBatch(requestId, responseSourceMessageId, requestPayloadSha256, existingBatch);
        }

        LocalDateTime now = nowUtc();
        String safeRequestId = optionalText(requestId, "request_id", LENGTH_128);
        ReservationAiBatchDraft batchDraft = new ReservationAiBatchDraft(
                hotelId,
                sourceMessageId,
                requestPayloadSha256,
                batchIdempotencyKey,
                requireText(clientId, "clientId", LENGTH_128),
                safeRequestId,
                now,
                taskResults.size(),
                nodeJson(root.get("extraction_warnings"))
        );
        Long batchId = insertBatchOrReplay(batchDraft);
        if (batchId == null) {
            return handleExistingBatch(
                    requestId,
                    responseSourceMessageId,
                    requestPayloadSha256,
                    workflowRepository.findBatchBySourceMessageId(hotelId, sourceMessageId)
                            .orElseThrow(() -> error(HttpStatus.CONFLICT, "IDEMPOTENCY_CONFLICT", "AI 批次并发写入状态不确定。")));
        }

        List<SuperAgentTaskResultItemResponse> responseItems = new ArrayList<>(taskResults.size());
        for (int index = 0; index < taskResults.size(); index++) {
            JsonNode item = taskResults.get(index);
            responseItems.add(createItem(hotelId, sourceMessageId, batchId, item, index + 1, now));
        }

        return new SuperAgentTaskResultResponse(
                safeRequestId,
                responseSourceMessageId,
                batchId.toString(),
                false,
                responseItems.size(),
                responseItems,
                List.of()
        );
    }

    /**
     * 接收 V3 结构化 S10/S99 来源邮件通知，复用只读特殊任务，不进入订单列表和执行队列。
     */
    private SuperAgentTaskResultResponse acceptV3SourceMessageNotification(
            JsonNode root,
            String clientId,
            String requestId,
            String defaultHotelId,
            String rawBody) {
        String routeCode = trimToNull(textAt(root, "route_code"));
        if (routeCode == null) {
            throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10/S99 route_code 缺失。");
        }
        validateLength(routeCode, "route_code", LENGTH_64);
        ReservationAiRouteDefinition route = ReservationAiRouteDefinition.findByRouteCode(routeCode)
                .filter(ReservationAiRouteDefinition::sourceMessageNotification)
                .orElseThrow(() -> error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10/S99 route_code 无效。"));
        if (!AiResultType.SOURCE_MESSAGE_REVIEW_NOTIFICATION.code().equals(textAt(root, "result_type"))) {
            throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10/S99 result_type 无效。");
        }
        validateV3SourceMessageNotificationShape(root, route);
        ResolvedSourceMessage resolvedSourceMessage = resolveV3SourceMessage(root, defaultHotelId);
        SourceMessageOnlyEntryResult entryResult = new SourceMessageOnlyEntryResult(
                SourceMessageOnlyResultCode.fromCode(route.taskSubtype())
                        .orElseThrow(() -> error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10/S99 结果码无效。")),
                resolvedSourceMessage.responseSourceMessageId(),
                AiResultType.SOURCE_MESSAGE_REVIEW_NOTIFICATION.code(),
                route.routeCode()
        );
        return acceptSourceMessageOnlyEntryResult(
                entryResult,
                clientId,
                requestId,
                defaultHotelId,
                rawBody,
                resolvedSourceMessage.snapshot());
    }

    /**
     * 接收 V3 业务根，按 message_events[] 逐项派生三元组；不完整或暂不支持的 event 只落契约错误。
     */
    private SuperAgentTaskResultResponse acceptV3BusinessRoot(
            JsonNode root,
            String clientId,
            String requestId,
            String defaultHotelId,
            String rawBody) {
        V3EventContractIssue rootIssue = inspectV3BusinessRootIssue(root);
        if (rootIssue != null) {
            throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", rootIssue.message());
        }
        ResolvedSourceMessage resolvedSourceMessage = resolveV3SourceMessage(root, defaultHotelId);
        SourceMessageInboxSnapshot sourceMessage = resolvedSourceMessage.snapshot();
        String hotelId = sourceMessage.hotelId();
        Long sourceMessageId = sourceMessage.id();
        JsonNode messageEvents = root.path("message_events");
        JsonNode unhandledIntents = root.path("unhandled_current_intents");
        int itemCount = messageEvents.size() + unhandledIntents.size();
        if (itemCount <= 0) {
            throw error(HttpStatus.BAD_REQUEST, "TASK_RESULTS_EMPTY", "message_events 和 unhandled_current_intents 不能同时为空。");
        }
        String requestPayloadSha256 = sha256(rawBody == null ? "" : rawBody);
        String batchIdempotencyKey = sha256(BATCH_KEY_PREFIX + "|" + sourceMessageId + "|" + requestPayloadSha256);

        ReservationAiBatchSnapshot existingBatch = workflowRepository
                .findBatchBySourceMessageId(hotelId, sourceMessageId)
                .orElse(null);
        if (existingBatch != null) {
            return handleExistingBatch(
                    requestId,
                    resolvedSourceMessage.responseSourceMessageId(),
                    requestPayloadSha256,
                    existingBatch);
        }

        LocalDateTime now = nowUtc();
        String safeRequestId = optionalText(requestId, "request_id", LENGTH_128);
        ReservationAiBatchDraft batchDraft = new ReservationAiBatchDraft(
                hotelId,
                sourceMessageId,
                requestPayloadSha256,
                batchIdempotencyKey,
                requireText(clientId, "clientId", LENGTH_128),
                safeRequestId,
                now,
                itemCount,
                nodeJson(root.get("extraction_warnings"))
        );
        Long batchId = insertBatchOrReplay(batchDraft);
        if (batchId == null) {
            return handleExistingBatch(
                    requestId,
                    resolvedSourceMessage.responseSourceMessageId(),
                    requestPayloadSha256,
                    workflowRepository.findBatchBySourceMessageId(hotelId, sourceMessageId)
                            .orElseThrow(() -> error(HttpStatus.CONFLICT, "IDEMPOTENCY_CONFLICT", "AI 批次并发写入状态不确定。")));
        }

        List<SuperAgentTaskResultItemResponse> responseItems = new ArrayList<>(itemCount);
        Set<String> acceptedParentSplitCodes = new LinkedHashSet<>();
        for (int index = 0; index < messageEvents.size(); index++) {
            JsonNode rawEvent = messageEvents.get(index);
            JsonNode event = normalizeV3ParentSplitEvent(rawEvent);
            int arrayIndex = index + 1;
            V3EventContractIssue contractIssue = inspectV3EventContractIssue(event, messageEvents);
            if (contractIssue == null) {
                contractIssue = inspectV3ParentSplitDuplicateIssue(event, acceptedParentSplitCodes);
            }
            if (contractIssue != null) {
                responseItems.add(createAdapterContractErrorTransition(
                        hotelId,
                        sourceMessageId,
                        batchId,
                        rawEvent,
                        arrayIndex,
                        sourceEventIndex(rawEvent, arrayIndex),
                        null,
                        contractIssue.code(),
                        contractIssue.message(),
                        now));
                continue;
            }
            ReservationAiRouteDefinition route = deriveV3EventRoute(event);
            if (route == null) {
                responseItems.add(createAdapterContractErrorTransition(
                        hotelId,
                        sourceMessageId,
                        batchId,
                        rawEvent,
                        arrayIndex,
                        sourceEventIndex(rawEvent, arrayIndex),
                        null,
                        "EVENT_ROUTE_UNSUPPORTED",
                        "message_events[" + index + "] 无法派生稳定路由。",
                        now));
                continue;
            }
            ObjectNode item = normalizeV3EventToTaskResultItem(rawEvent, event, route, arrayIndex);
            responseItems.add(createItem(hotelId, sourceMessageId, batchId, item, arrayIndex, now));
        }
        for (int index = 0; index < unhandledIntents.size(); index++) {
            int arrayIndex = messageEvents.size() + index + 1;
            responseItems.add(createAdapterContractErrorTransition(
                    hotelId,
                    sourceMessageId,
                    batchId,
                    unhandledIntents.get(index),
                    arrayIndex,
                    arrayIndex,
                    ReservationAiRouteDefinition.UNHANDLED_CURRENT_INTENT,
                    null,
                    null,
                    now));
        }

        return new SuperAgentTaskResultResponse(
                safeRequestId,
                resolvedSourceMessage.responseSourceMessageId(),
                batchId.toString(),
                false,
                responseItems.size(),
                responseItems,
                List.of()
        );
    }

    /**
     * 校验 V2 兼容 ai_task_results[] 中不再允许当前 producer 继续提交的旧 Parent split 三元组。
     */
    private V3EventContractIssue inspectV2TaskResultsContractIssue(JsonNode taskResults) {
        for (JsonNode item : taskResults) {
            if (isLegacyParentSplitTriplet(
                    textAt(item, "result_type"),
                    textAt(item, "task_type"),
                    textAt(item, "task_subtype"))) {
                return new V3EventContractIssue(
                        "LINKED_PARENT_RELEASE_LEGACY_CANCEL_BOOKING_UNSUPPORTED",
                        "当前 producer 不再支持 Cancel Booking + linked_parent_release_after_child_split，Parent split 父事件必须使用 Cancel Allotment。");
            }
        }
        return null;
    }

    /**
     * 识别 P0.1 已删除的旧 Parent split 三元组。
     */
    private boolean isLegacyParentSplitTriplet(String resultType, String taskType, String taskSubtype) {
        return ("normal_task".equals(resultType) || "manual_review".equals(resultType))
                && "Cancel Booking".equals(taskType)
                && "linked_parent_release_after_child_split".equals(taskSubtype);
    }

    /**
     * 校验 V3 业务根的批次级 P0 结构；单个事件内容错误由 event 级 transition 承接。
     */
    private V3EventContractIssue inspectV3BusinessRootIssue(JsonNode root) {
        if (!hasExactFields(root, V3_BUSINESS_ROOT_KEYS)
                || !root.path("message_events").isArray()
                || !root.path("case_candidates").isArray()
                || !root.path("extraction_warnings").isArray()
                || !root.path("unhandled_current_intents").isArray()) {
            return new V3EventContractIssue("BUSINESS_ROOT_CONTRACT_INVALID", "V3 业务根结构不符合 0711 P0 契约。");
        }
        V3EventContractIssue sourceMessageIssue = inspectV3SourceMessageIssue(root.path("source_message"));
        if (sourceMessageIssue != null) {
            return sourceMessageIssue;
        }
        return null;
    }

    /**
     * 校验结构化 S10/S99 的最小形态，避免把联合值或空复核误收为正常通知。
     */
    private void validateV3SourceMessageNotificationShape(JsonNode root, ReservationAiRouteDefinition route) {
        V3EventContractIssue sourceMessageIssue = inspectV3SourceMessageIssue(root.path("source_message"));
        if (!hasExactFields(root, V3_MAIN_OUTCOME_KEYS)
                || sourceMessageIssue != null
                || !hasExactFields(root.path("agent_assessment"), V3_AGENT_ASSESSMENT_KEYS)
                || !hasExactFields(root.path("notification"), V3_NOTIFICATION_KEYS)
                || !"main_agent_outcome".equals(textAt(root, "handler_type"))
                || !AiResultType.SOURCE_MESSAGE_REVIEW_NOTIFICATION.code().equals(textAt(root, "result_type"))
                || !"current".equals(textAt(root, "current_or_history"))) {
            throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10/S99 根结构字段无效。");
        }
        JsonNode assessment = root.path("agent_assessment");
        JsonNode notification = root.path("notification");
        if (!"none".equals(textAt(assessment, "automation_action"))
                || !isBooleanTrue(notification.path("required"))
                || !"source_message_review".equals(textAt(notification, "notification_type"))
                || !isBooleanTrue(notification.path("show_source_message"))
                || !isBooleanTrue(notification.path("requires_user_decision"))
                || trimToNull(textAt(notification, "visible_message")) == null) {
            throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10/S99 通知结构无效。");
        }
        JsonNode manualReview = root.get("manual_review");
        if (route == ReservationAiRouteDefinition.SOURCE_MESSAGE_S10) {
            if (!"no_booking_action_detected".equals(textAt(assessment, "status"))
                    || !"no_booking_action_detected".equals(textAt(assessment, "reason_code"))
                    || (manualReview != null && !manualReview.isNull())) {
                throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S10 结构不符合 P0 契约。");
            }
            return;
        }
        if (!"material_package_unavailable".equals(textAt(assessment, "status"))
                || !"material_package_unavailable".equals(textAt(assessment, "reason_code"))
                || manualReview == null
                || manualReview.isNull()
                || !manualReview.isObject()
                || inspectV3MainAgentReviewIssue(manualReview) != null
                || !"material_package_unavailable".equals(textAt(manualReview, "reason_code"))) {
            throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", "S99 结构不符合 P0 契约。");
        }
    }

    /**
     * 校验 S99 main_agent_entry_review 的 P0 九字段结构。
     */
    private V3EventContractIssue inspectV3MainAgentReviewIssue(JsonNode manualReview) {
        return inspectV3ReviewIssue(
                manualReview,
                "main_agent_entry_review",
                null,
                "MAIN_AGENT_REVIEW_INVALID",
                "MAIN_AGENT_REVIEW_INCOMPLETE",
                "MAIN_AGENT_REVIEW_MISSING_FIELD_POINTER_INVALID");
    }

    /**
     * 校验 V3 SourceMessage 元数据；source_message_id 缺失由入口预检查返回 typed infrastructure error。
     */
    private V3EventContractIssue inspectV3SourceMessageIssue(JsonNode sourceMessage) {
        if (!hasExactFields(sourceMessage, V3_SOURCE_MESSAGE_KEYS)
                || trimToNull(textAt(sourceMessage, "source_message_id")) == null
                || !isStringOrNull(sourceMessage.path("subject"))
                || !isStringOrNull(sourceMessage.path("from"))
                || !isStringArray(sourceMessage.path("cc"), false)
                || !isStringOrNull(sourceMessage.path("received_at"))
                || !"Email".equals(textAt(sourceMessage, "source_channel"))) {
            return new V3EventContractIssue("SOURCE_MESSAGE_CONTRACT_INVALID", "source_message 结构不符合 0711 P0 契约。");
        }
        return null;
    }

    /**
     * 校验 V3 单个 message_event 的基础形态；缺字段时 fail closed，不猜测任务含义。
     */
    private V3EventContractIssue inspectV3BusinessEventShapeIssue(JsonNode event) {
        if (event == null || !event.isObject() || !hasRequiredFields(event, V3_EVENT_REQUIRED_KEYS)) {
            return new V3EventContractIssue("EVENT_CONTRACT_INCOMPLETE", "message_event 必填字段不完整。");
        }
        if ((!V3_ACTIVE_EVENT_TYPES.contains(textAt(event, "event_type"))
                && !"Need Manual Review".equals(textAt(event, "event_type")))
                || trimToNull(textAt(event, "event_role")) == null
                || !"current".equals(textAt(event, "current_or_history"))
                || trimToNull(textAt(event, "source_event_index")) == null
                || inspectV3CaseKeysIssue(event.path("case_keys")) != null
                || !event.path("relevant_message_excerpt").isTextual()
                || !event.path("attachments").isArray()
                || !event.path("file_references").isArray()
                || !event.path("context_used").isObject()
                || !event.path("extracted_fields").isObject()) {
            return new V3EventContractIssue("EVENT_CONTRACT_INCOMPLETE", "message_event 字段类型或基础值无效。");
        }
        JsonNode manualReview = event.get("manual_review");
        if (!manualReview.isNull() && !manualReview.isObject()) {
            return new V3EventContractIssue("EVENT_CONTRACT_INCOMPLETE", "message_event.manual_review 必须为 null 或对象。");
        }
        if ("Need Manual Review".equals(textAt(event, "event_type")) && manualReview.isNull()) {
            return new V3EventContractIssue("EVENT_CONTRACT_INCOMPLETE", "Need Manual Review 必须携带 manual_review。");
        }
        return null;
    }

    /**
     * 校验 case_keys 的 P0 四字段结构。
     */
    private V3EventContractIssue inspectV3CaseKeysIssue(JsonNode caseKeys) {
        if (!hasExactFields(caseKeys, V3_CASE_KEY_KEYS)) {
            return new V3EventContractIssue("CASE_KEYS_CONTRACT_INVALID", "case_keys 结构不符合 0711 P0 契约。");
        }
        for (String key : V3_CASE_KEY_KEYS) {
            if (!isStringOrNull(caseKeys.path(key))) {
                return new V3EventContractIssue("CASE_KEYS_CONTRACT_INVALID", "case_keys 字段必须为 string 或 null。");
            }
        }
        return null;
    }

    /**
     * 复用 0711 P0 manual_review 九字段结构校验，业务事件复核还要确认缺失字段 pointer 能定位到当前事件。
     */
    private V3EventContractIssue inspectV3ReviewIssue(
            JsonNode manualReview,
            String expectedRecordType,
            JsonNode businessEvent,
            String invalidCode,
            String incompleteCode,
            String pointerInvalidCode) {
        if (manualReview == null || !manualReview.isObject()) {
            return new V3EventContractIssue(invalidCode, "manual_review 必须是对象。");
        }
        if (!hasExactFields(manualReview, V3_REVIEW_KEYS)
                || trimToNull(textAt(manualReview, "reason_code")) == null
                || trimToNull(textAt(manualReview, "visible_reason")) == null
                || !expectedRecordType.equals(textAt(manualReview, "review_record_type"))
                || !manualReview.path("known_fields").isObject()) {
            return new V3EventContractIssue(incompleteCode, "manual_review 九字段不完整。");
        }
        for (String arrayKey : V3_REVIEW_ARRAY_KEYS) {
            if (!isStringArray(manualReview.path(arrayKey), false)) {
                return new V3EventContractIssue(incompleteCode, "manual_review 数组字段必须只包含字符串。");
            }
        }
        if (businessEvent != null) {
            for (JsonNode pointer : manualReview.path("missing_fields")) {
                String pointerValue = pointer.isTextual() ? pointer.asText() : null;
                if (!jsonPointerTargetExists(businessEvent, pointerValue)) {
                    return new V3EventContractIssue(
                            pointerInvalidCode,
                            "manual_review.missing_fields 必须使用能定位当前事件字段的 RFC 6901 JSON Pointer。");
                }
            }
        }
        return null;
    }


    /**
     * 将 V3 业务 event 转为旧任务结果 item 形态，业务列使用 adapter 派生 event，原始 event 保留在 v3_message_event。
     */
    private ObjectNode normalizeV3EventToTaskResultItem(
            JsonNode rawEvent,
            JsonNode derivedEvent,
            ReservationAiRouteDefinition route,
            int arrayIndex) {
        ObjectNode item = objectMapper.createObjectNode();
        item.put("source_event_index", sourceEventIndex(rawEvent, arrayIndex));
        item.put("catalog_code", V3_CATALOG_CODE);
        item.put("skill_id", V3_SKILL_ID);
        item.put("result_type", route.resultType());
        item.put("task_type", route.taskType());
        item.put("task_subtype", route.taskSubtype());
        copyIfPresent(rawEvent, item, "current_or_history");
        copyIfPresent(rawEvent, item, "relevant_message_excerpt");
        copyIfPresent(derivedEvent, item, "case_keys");
        copyIfPresent(rawEvent, item, "extracted_fields");
        copyIfPresent(rawEvent, item, "manual_review");
        copyIfPresent(rawEvent, item, "attachments");
        copyIfPresent(rawEvent, item, "file_references");
        copyIfPresent(rawEvent, item, "context_used");
        copyIfPresent(rawEvent, item, "parent_source_event_index");
        copyIfPresent(rawEvent, item, "linked_task_group_id");
        copyIfPresent(rawEvent, item, "blocked_until_parent_completed");
        item.set("v3_message_event", rawEvent);
        item.put("route_code", route.routeCode());
        item.put("system_process_category", route.systemProcessCategory().name());
        return item;
    }

    /**
     * 对 V3 event 做最小路由派生，未覆盖或判别字段不足时返回 null，由调用方落契约错误。
     */
    private ReservationAiRouteDefinition deriveV3EventRoute(JsonNode event) {
        String eventType = trimToNull(textAt(event, "event_type"));
        String resultType = isNullOrMissing(event.get("manual_review"))
                ? AiResultType.NORMAL_TASK.code()
                : AiResultType.MANUAL_REVIEW.code();
        String taskType = deriveTaskType(eventType);
        String taskSubtype = deriveTaskSubtype(event, eventType);
        if (taskType == null || taskSubtype == null) {
            return null;
        }
        return ReservationAiRouteDefinition.findByTriplet(resultType, taskType, taskSubtype).orElse(null);
    }

    /**
     * 按 V3 event_type 派生系统使用的 AI task_type。
     */
    private String deriveTaskType(String eventType) {
        if ("Update Booking / Amendment".equals(eventType)) {
            return "Update Booking";
        }
        if ("AMEND GROUP CODE".equals(eventType)) {
            return "Amend Group Code";
        }
        if ("Trace".equals(eventType)) {
            return "Trace / Reservation Notes";
        }
        if ("TA RECORDER".equals(eventType)) {
            return "TA Recorder";
        }
        if ("Need Manual Review".equals(eventType)) {
            return "Fallback";
        }
        return eventType;
    }

    /**
     * 按 V3 event_type 和判别字段派生 task_subtype；字段不足时返回 null 并 fail closed。
     */
    private String deriveTaskSubtype(JsonNode event, String eventType) {
        JsonNode extractedFields = event.path("extracted_fields");
        return switch (nullToEmpty(eventType)) {
            case "New Booking" -> switch (nullToEmpty(firstText(extractedFields, event, "booking_object_type"))) {
                case "FIT Reservation" -> "new_fit_reservation";
                case "Group Block" -> "new_group_block";
                case "Allotment / Control Block" -> "new_allotment_control_block";
                default -> null;
            };
            case "Update Booking / Amendment" -> "update_booking_amendment";
            case "Cancel Booking" -> cancelBookingSubtype(event, extractedFields);
            case "Cancel Allotment" -> "cancel_allotment_control_block";
            case "Voucher Received" -> "lian_tai_credit_voucher";
            case "Payment Evidence" -> "payment_evidence";
            case "Rooming List" -> "rooming_list";
            case "AMEND GROUP CODE" -> "amend_group_code";
            case "Trace" -> switch (nullToEmpty(firstText(extractedFields, event, "trace_subtype"))) {
                case "extra_bed" -> "extra_bed";
                case "general_request" -> "general_request";
                default -> null;
            };
            case "TA RECORDER" -> "maintain_ta_recorder";
            case "Invoice Generation" -> "invoice_generation";
            case "Invoice Received" -> "invoice_received";
            case "Payment Notice" -> "payment_notice";
            case "Manual RateCode" -> "manual_rate_code";
            case "Need Manual Review" -> "business_event_review";
            default -> null;
        };
    }

    /**
     * Cancel Booking 只识别普通取消对象；Parent split 在 P0.1 后统一走 Cancel Allotment。
     */
    private String cancelBookingSubtype(JsonNode event, JsonNode extractedFields) {
        return switch (nullToEmpty(firstText(extractedFields, event, "cancel_object_type"))) {
            case "fit_reservation" -> "cancel_fit_reservation";
            case "group_block" -> "cancel_group_block";
            default -> null;
        };
    }

    /**
     * 识别 V3 event 中已经明确暴露的契约问题。命中后该 event 不建业务卡，只保存 transition。
     */
    private V3EventContractIssue inspectV3EventContractIssue(JsonNode event, JsonNode messageEvents) {
        if (nonEmptyArray(event, "contract_errors")) {
            return new V3EventContractIssue("EVENT_CONTRACT_ERROR", "message_event 携带 contract_errors，第一版不建业务任务。");
        }
        if (nonEmptyArray(event, "missing_fields")) {
            return new V3EventContractIssue("EVENT_CONTRACT_INCOMPLETE", "message_event 根节点携带 missing_fields，第一版不建业务任务。");
        }
        V3EventContractIssue shapeIssue = inspectV3BusinessEventShapeIssue(event);
        if (shapeIssue != null) {
            return shapeIssue;
        }
        JsonNode manualReview = event == null ? null : event.get("manual_review");
        if (!isNullOrMissing(manualReview)) {
            V3EventContractIssue manualReviewIssue = inspectV3ManualReviewContractIssue(manualReview, event);
            if (manualReviewIssue != null) {
                return manualReviewIssue;
            }
        }
        if (isLegacyLinkedParentCancelBooking(event)) {
            return new V3EventContractIssue(
                    "LINKED_PARENT_RELEASE_LEGACY_CANCEL_BOOKING_UNSUPPORTED",
                    "当前 producer 不再支持 Cancel Booking + linked_parent_release_after_child_split，Parent split 父事件必须使用 Cancel Allotment。");
        }
        if (isLinkedParentReleaseCandidate(event) && !validLinkedParentReleaseCandidate(event, messageEvents)) {
            return new V3EventContractIssue(
                    "LINKED_PARENT_RELEASE_CONTRACT_INCOMPLETE",
                    "linked_parent_release_after_child_split 关系字段不完整，第一版不建业务任务。");
        }
        return null;
    }

    /**
     * 校验 V3 type-known manual_review 的九字段基础结构，后续同卡复核解阻会继续校验缺失字段。
     */
    private V3EventContractIssue inspectV3ManualReviewContractIssue(JsonNode manualReview, JsonNode event) {
        return inspectV3ReviewIssue(
                manualReview,
                "business_event_review",
                event,
                "MANUAL_REVIEW_CONTRACT_INVALID",
                "MANUAL_REVIEW_CONTRACT_INCOMPLETE",
                "MANUAL_REVIEW_MISSING_FIELD_POINTER_INVALID");
    }

    /**
     * 判断是否为 parent release / cancel 派生事件候选。
     */
    private boolean isLinkedParentReleaseCandidate(JsonNode event) {
        JsonNode extractedFields = event == null ? null : event.path("extracted_fields");
        return "linked_parent_release_after_child_split".equals(firstText(extractedFields, event, "relationship_type"));
    }

    /**
     * 归一 Parent split 的 Parent Group key；只补齐 adapter 派生用副本，不修改请求原文。
     */
    private JsonNode normalizeV3ParentSplitEvent(JsonNode event) {
        if (event == null || !event.isObject() || !isLinkedParentReleaseCandidate(event)) {
            return event;
        }
        ObjectNode normalizedEvent = event.deepCopy();
        JsonNode caseKeysNode = normalizedEvent.path("case_keys");
        if (!caseKeysNode.isObject()) {
            return normalizedEvent;
        }
        ObjectNode caseKeys = (ObjectNode) caseKeysNode;
        String groupCode = trimToNull(textAt(caseKeys, "group_code"));
        String blockCode = trimToNull(textAt(caseKeys, "block_code"));
        if (groupCode == null && blockCode != null) {
            caseKeys.put("group_code", blockCode);
            return normalizedEvent;
        }
        if (groupCode != null && blockCode == null) {
            caseKeys.put("block_code", groupCode);
            return normalizedEvent;
        }
        if (groupCode != null && blockCode != null && !groupCode.equals(blockCode)) {
            caseKeys.putNull("group_code");
            caseKeys.putNull("block_code");
        }
        return normalizedEvent;
    }

    /**
     * 判断当前 producer 是否仍在输出旧 Parent Cancel Booking 组合。
     */
    private boolean isLegacyLinkedParentCancelBooking(JsonNode event) {
        JsonNode extractedFields = event == null ? null : event.path("extracted_fields");
        return isLegacyParentSplitTriplet(
                event == null || isNullOrMissing(event.get("manual_review"))
                        ? AiResultType.NORMAL_TASK.code()
                        : AiResultType.MANUAL_REVIEW.code(),
                trimToNull(textAt(event, "event_type")),
                firstText(extractedFields, event, "relationship_type"));
    }

    /**
     * 校验同一 V3 业务根中同一个 Parent split cluster 只能有一个 Parent 候选。
     */
    private V3EventContractIssue inspectV3ParentSplitDuplicateIssue(JsonNode event, Set<String> acceptedParentSplitCodes) {
        if (!isLinkedParentReleaseCandidate(event)) {
            return null;
        }
        ParentSplitIdentity identity = parentSplitIdentity(event);
        if (identity.normalizedParentCode() != null && !acceptedParentSplitCodes.add(identity.normalizedParentCode())) {
            return new V3EventContractIssue(
                    "LINKED_PARENT_RELEASE_DUPLICATE_PARENT",
                    "同一个 Parent split cluster 只能有一个 Parent Cancel Allotment 候选。");
        }
        return null;
    }

    /**
     * 校验 P0.1 parent split 候选的最小可追溯关系字段，避免只凭 relationship_type 建业务任务。
     */
    private boolean validLinkedParentReleaseCandidate(JsonNode event, JsonNode messageEvents) {
        JsonNode extractedFields = event == null ? null : event.path("extracted_fields");
        ParentSplitIdentity identity = parentSplitIdentity(event);
        if (!("Cancel Allotment".equals(trimToNull(textAt(event, "event_type")))
                && "entire_allotment_control_block".equals(firstText(extractedFields, event, "cancel_scope"))
                && isBooleanTrue(extractedFields.path("parent_release_or_cancel_candidate"))
                && "parent_to_child_allocation_split".equals(firstText(extractedFields, event, "release_reason"))
                && isBooleanTrue(extractedFields.path("allocation_split_from_parent"))
                && "New Booking".equals(trimToNull(textAt(event, "related_event_type")))
                && event != null
                && isBooleanTrue(event.path("requires_downstream_hard_validation"))
                && nonEmptyArray(extractedFields, "child_group_codes")
                && nonEmptyArray(event, "related_source_event_indices"))) {
            return false;
        }
        if (!identity.targetObjectUnclearReview() && !validConcreteParentIdentity(identity, extractedFields)) {
            return false;
        }
        if (identity.targetObjectUnclearReview() && !nonEmptyArray(event.path("context_used"), "parent_identity_candidates")) {
            return false;
        }
        List<String> childGroupCodes = stringArrayValues(extractedFields.path("child_group_codes"));
        List<String> relatedSourceEventIndices = stringArrayValues(event.path("related_source_event_indices"));
        if (childGroupCodes.isEmpty()
                || relatedSourceEventIndices.isEmpty()
                || childGroupCodes.size() != relatedSourceEventIndices.size()) {
            return false;
        }
        Set<String> childGroupCodeSet = new LinkedHashSet<>(childGroupCodes);
        Set<String> relatedEventIds = new LinkedHashSet<>(relatedSourceEventIndices);
        if (childGroupCodeSet.size() != childGroupCodes.size()
                || relatedEventIds.size() != relatedSourceEventIndices.size()) {
            return false;
        }
        for (int index = 0; index < relatedSourceEventIndices.size(); index++) {
            String sourceEventIndex = relatedSourceEventIndices.get(index);
            JsonNode childEvent = findV3MessageEventBySourceEventIndex(messageEvents, sourceEventIndex);
            if (childEvent == null || !"New Booking".equals(trimToNull(textAt(childEvent, "event_type")))) {
                return false;
            }
            JsonNode childExtractedFields = childEvent.path("extracted_fields");
            if (!"Group Block".equals(firstText(childExtractedFields, childEvent, "booking_object_type"))) {
                return false;
            }
            String childGroupCode = trimToNull(textAt(childEvent.path("case_keys"), "group_code"));
            if (!childGroupCodes.get(index).equals(childGroupCode)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 校验普通 Parent split 事件是否已经具备明确 Parent Group identity。
     */
    private boolean validConcreteParentIdentity(ParentSplitIdentity identity, JsonNode extractedFields) {
        return identity.normalizedParentCode() != null
                && identity.normalizedParentCode().equals(identity.groupCode())
                && identity.normalizedParentCode().equals(identity.blockCode())
                && identity.normalizedParentCode().equals(trimToNull(textAt(extractedFields, "parent_group_code")));
    }

    /**
     * 提取 Parent split identity，封装正常归一与 target_object_unclear 复核两种形态。
     */
    private ParentSplitIdentity parentSplitIdentity(JsonNode event) {
        JsonNode caseKeys = event == null ? null : event.path("case_keys");
        String groupCode = trimToNull(textAt(caseKeys, "group_code"));
        String blockCode = trimToNull(textAt(caseKeys, "block_code"));
        String normalizedParentCode = groupCode != null && groupCode.equals(blockCode) ? groupCode : null;
        boolean targetObjectUnclearReview = groupCode == null
                && blockCode == null
                && "target_object_unclear".equals(textAt(event == null ? null : event.path("manual_review"), "reason_code"));
        return new ParentSplitIdentity(groupCode, blockCode, normalizedParentCode, targetObjectUnclearReview);
    }

    /**
     * 接收 S000/S999 文本入口结果，创建只读来源消息任务，并隐藏其技术订单。
     */
    private SuperAgentTaskResultResponse acceptSourceMessageOnlyEntryResult(
            SourceMessageOnlyEntryResult entryResult,
            String clientId,
            String requestId,
            String defaultHotelId,
            String rawBody) {
        return acceptSourceMessageOnlyEntryResult(entryResult, clientId, requestId, defaultHotelId, rawBody, null);
    }

    /**
     * 接收来源邮件只读结果。V3 已解析 SourceMessage 时直接复用，旧文本结果按默认酒店反查。
     */
    private SuperAgentTaskResultResponse acceptSourceMessageOnlyEntryResult(
            SourceMessageOnlyEntryResult entryResult,
            String clientId,
            String requestId,
            String defaultHotelId,
            String rawBody,
            SourceMessageInboxSnapshot resolvedSourceMessage) {
        String hotelId = resolvedSourceMessage == null
                ? requireText(defaultHotelId, "default_hotel_id", LENGTH_64)
                : requireText(resolvedSourceMessage.hotelId(), "source_message.hotel_id", LENGTH_64);
        SourceMessageInboxSnapshot sourceMessage = resolvedSourceMessage == null
                ? resolveExternalSourceMessageByHotel(hotelId, entryResult.externalSourceMessageId())
                : resolvedSourceMessage;
        String requestPayloadSha256 = sha256(rawBody == null ? "" : rawBody);
        String batchIdempotencyKey = sha256(BATCH_KEY_PREFIX + "|" + sourceMessage.id() + "|" + requestPayloadSha256);
        ReservationAiBatchSnapshot existingBatch = workflowRepository
                .findBatchBySourceMessageId(hotelId, sourceMessage.id())
                .orElse(null);
        if (existingBatch != null) {
            return handleExistingBatch(
                    requestId,
                    entryResult.externalSourceMessageId(),
                    requestPayloadSha256,
                    existingBatch);
        }

        LocalDateTime now = nowUtc();
        String safeRequestId = optionalText(requestId, "request_id", LENGTH_128);
        ReservationAiBatchDraft batchDraft = new ReservationAiBatchDraft(
                hotelId,
                sourceMessage.id(),
                requestPayloadSha256,
                batchIdempotencyKey,
                requireText(clientId, "clientId", LENGTH_128),
                safeRequestId,
                now,
                1,
                null
        );
        Long batchId = insertBatchOrReplay(batchDraft);
        if (batchId == null) {
            return handleExistingBatch(
                    requestId,
                    entryResult.externalSourceMessageId(),
                    requestPayloadSha256,
                    workflowRepository.findBatchBySourceMessageId(hotelId, sourceMessage.id())
                            .orElseThrow(() -> error(HttpStatus.CONFLICT, "IDEMPOTENCY_CONFLICT", "AI 批次并发写入状态不确定。")));
        }

        SuperAgentTaskResultItemResponse item = createSourceMessageOnlyItem(
                hotelId,
                sourceMessage.id(),
                batchId,
                entryResult,
                rawBody,
                now);
        return new SuperAgentTaskResultResponse(
                safeRequestId,
                entryResult.externalSourceMessageId(),
                batchId.toString(),
                false,
                1,
                List.of(item),
                List.of());
    }

    /**
     * 创建 S000/S999 对应的只读任务。该任务只用于任务列表和详情展示，不参与订单执行队列。
     */
    private SuperAgentTaskResultItemResponse createSourceMessageOnlyItem(
            String hotelId,
            Long sourceMessageId,
            Long batchId,
            SourceMessageOnlyEntryResult entryResult,
            String rawBody,
            LocalDateTime now) {
        ObjectNode itemPayload = objectMapper.createObjectNode();
        itemPayload.put("entry_result_code", entryResult.resultCode().code());
        itemPayload.put("entry_result_meaning", entryResult.resultCode().meaning());
        itemPayload.put("entry_result_description", entryResult.resultCode().description());
        itemPayload.put("source_message_id", entryResult.externalSourceMessageId());
        itemPayload.put("result_type", entryResult.resultTypeCode());
        itemPayload.put("route_code", entryResult.routeCode());
        itemPayload.put("raw_answer", rawBody);
        String itemPayloadJson = nodeJson(itemPayload);
        String itemPayloadSha256 = sha256(itemPayloadJson);
        String itemIdempotencyKey = sha256(ITEM_KEY_PREFIX
                + "|" + sourceMessageId
                + "|1|1|"
                + SOURCE_MESSAGE_ONLY_CATALOG_CODE
                + "|"
                + SOURCE_MESSAGE_ONLY_SKILL_ID
                + "|"
                + entryResult.resultTypeCode()
                + "|"
                + entryResult.resultCode().code()
                + "|"
                + entryResult.resultCode().code()
                + "|"
                + itemPayloadSha256);
        Long transitionId = workflowRepository.insertTransition(new ReservationAiTransitionDraft(
                hotelId,
                batchId,
                sourceMessageId,
                1,
                1,
                1,
                SOURCE_MESSAGE_ONLY_CATALOG_CODE,
                SOURCE_MESSAGE_ONLY_SKILL_ID,
                entryResult.resultTypeCode(),
                entryResult.resultCode().code(),
                entryResult.routeCode(),
                ReservationAiSystemProcessCategory.SOURCE_MESSAGE_NOTIFICATION.name(),
                ReservationSystemTaskType.SOURCE_MESSAGE_ONLY.name(),
                ReservationTaskCardType.SOURCE_MESSAGE_ONLY.name(),
                entryResult.resultCode().code(),
                null,
                null,
                null,
                itemPayloadSha256,
                itemIdempotencyKey,
                null,
                null,
                null,
                Boolean.FALSE,
                itemPayloadJson,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                now
        ));
        ReservationOrderSnapshot order = createHiddenSourceMessageOnlyOrder(hotelId, sourceMessageId, now);
        TaskTypeMapping mapping = new TaskTypeMapping(
                ReservationSystemTaskType.SOURCE_MESSAGE_ONLY,
                ReservationTaskCardType.SOURCE_MESSAGE_ONLY);
        TaskCreation taskCreation = insertTaskWithQueueRetry(
                hotelId,
                sourceMessageId,
                transitionId,
                entryResult.resultTypeCode(),
                entryResult.resultCode().code(),
                mapping,
                entryResult.resultCode().code(),
                ReservationTaskStatus.COMPLETED.name(),
                false,
                null,
                null,
                Boolean.FALSE,
                now,
                order,
                now
        );
        workflowRepository.insertTaskCard(new ReservationTaskCardDraft(
                hotelId,
                taskCreation.taskId(),
                ReservationTaskCardType.SOURCE_MESSAGE_ONLY.name(),
                FIELD_CONTRACT_VERSION,
                itemPayloadJson,
                null,
                now
        ));
        return new SuperAgentTaskResultItemResponse(
                1,
                1,
                transitionId.toString(),
                entryResult.routeCode(),
                ReservationAiSystemProcessCategory.SOURCE_MESSAGE_NOTIFICATION.name(),
                null,
                order.id().toString(),
                taskCreation.taskId().toString(),
                ReservationSystemTaskType.SOURCE_MESSAGE_ONLY.name(),
                ReservationTaskCardType.SOURCE_MESSAGE_ONLY.name(),
                ReservationTaskStatus.COMPLETED.name(),
                order.orderStatus(),
                taskCreation.executionOrder()
        );
    }

    /**
     * 插入批次；如果并发请求已经写入同一 SourceMessage，则交由调用方按幂等规则重新读取处理。
     */
    private Long insertBatchOrReplay(ReservationAiBatchDraft batchDraft) {
        try {
            return workflowRepository.insertBatch(batchDraft);
        } catch (DuplicateKeyException exception) {
            return null;
        }
    }

    /**
     * 处理已存在批次：完全相同 payload 返回幂等重放，不同 payload 返回冲突。
     */
    private SuperAgentTaskResultResponse handleExistingBatch(
            String requestId,
            String responseSourceMessageId,
            String requestPayloadSha256,
            ReservationAiBatchSnapshot existingBatch) {
        if (!requestPayloadSha256.equals(existingBatch.requestPayloadSha256())) {
            throw error(HttpStatus.CONFLICT, "IDEMPOTENCY_CONFLICT", "同一 SourceMessage 已存在不同 AI 结果请求。");
        }
        return new SuperAgentTaskResultResponse(
                requestId,
                responseSourceMessageId,
                existingBatch.id().toString(),
                true,
                existingBatch.itemCount() == null ? 0 : existingBatch.itemCount(),
                List.of(),
                List.of(new SuperAgentTaskResultWarning("IDEMPOTENT_REPLAY", "相同请求已经处理，本次未重复创建任务。"))
        );
    }

    /**
     * 创建单条 AI item 对应的过渡记录、订单、任务和任务卡。
     */
    private SuperAgentTaskResultItemResponse createItem(
            String hotelId,
            Long sourceMessageId,
            Long batchId,
            JsonNode item,
            int arrayIndex,
            LocalDateTime now) {
        Integer sourceEventIndex = requirePositiveInt(item, "source_event_index");
        String catalogCode = requireText(textAt(item, "catalog_code"), "catalog_code", LENGTH_32);
        String skillId = requireText(textAt(item, "skill_id"), "skill_id", LENGTH_128);
        String resultType = requireText(textAt(item, "result_type"), "result_type", LENGTH_64);
        String aiTaskType = requireText(textAt(item, "task_type"), "task_type", LENGTH_64);
        String taskSubtype = optionalText(textAt(item, "task_subtype"), "task_subtype", LENGTH_128);
        TaskTypeMapping mapping = mapTaskType(resultType, aiTaskType, taskSubtype);
        ReservationAiRouteDefinition route = ReservationAiRouteDefinition
                .findByTriplet(resultType, aiTaskType, taskSubtype)
                .orElse(null);
        String routeCode = optionalText(textAt(item, "route_code"), "route_code", LENGTH_64);
        if (routeCode == null && route != null) {
            routeCode = route.routeCode();
        }
        String systemProcessCategory = optionalText(
                textAt(item, "system_process_category"),
                "system_process_category",
                LENGTH_64);
        if (systemProcessCategory == null) {
            systemProcessCategory = route == null
                    ? ReservationAiSystemProcessCategory.BUSINESS_TASK.name()
                    : route.systemProcessCategory().name();
        }
        JsonNode caseKeys = item.path("case_keys");
        String currentOrHistory = optionalText(textAt(item, "current_or_history"), "current_or_history", LENGTH_32);
        String groupCode = optionalText(textAt(caseKeys, "group_code"), "group_code", LENGTH_128);
        String confirmationNumber = optionalText(textAt(caseKeys, "confirmation_number"), "confirmation_number", LENGTH_128);
        String manualReasonCode = optionalText(textAt(item.path("manual_review"), "reason_code"), "manual_review.reason_code", LENGTH_128);
        String linkedTaskGroupId = optionalText(textAt(item, "linked_task_group_id"), "linked_task_group_id", LENGTH_128);
        String itemPayloadJson = nodeJson(item);
        String itemPayloadSha256 = sha256(itemPayloadJson);
        String itemIdempotencyKey = sha256(ITEM_KEY_PREFIX
                + "|" + sourceMessageId
                + "|" + sourceEventIndex
                + "|" + arrayIndex
                + "|" + catalogCode
                + "|" + skillId
                + "|" + resultType
                + "|" + aiTaskType
                + "|" + nullToEmpty(taskSubtype)
                + "|" + itemPayloadSha256);

        Long transitionId = workflowRepository.insertTransition(new ReservationAiTransitionDraft(
                hotelId,
                batchId,
                sourceMessageId,
                sourceEventIndex,
                arrayIndex,
                arrayIndex,
                catalogCode,
                skillId,
                resultType,
                aiTaskType,
                routeCode,
                systemProcessCategory,
                mapping.systemTaskType().name(),
                mapping.taskCardType().name(),
                taskSubtype,
                currentOrHistory,
                groupCode,
                confirmationNumber,
                itemPayloadSha256,
                itemIdempotencyKey,
                manualReasonCode,
                integerAt(item, "parent_source_event_index"),
                linkedTaskGroupId,
                booleanAt(item, "blocked_until_parent_completed"),
                itemPayloadJson,
                nodeJson(item.get("case_keys")),
                nodeJson(item.get("extracted_fields")),
                nodeJson(item.get("manual_review")),
                nodeJson(item.get("informational_message")),
                nodeJson(item.get("attachments")),
                nodeJson(item.get("context_used")),
                null,
                null,
                now
        ));

        ReservationOrderSnapshot order = resolveOrder(
                hotelId,
                sourceMessageId,
                mapping.systemTaskType(),
                groupCode,
                confirmationNumber,
                arrayIndex,
                now);
        boolean queueParticipation = mapping.systemTaskType() != ReservationSystemTaskType.INFORMATIONAL_MESSAGE;
        ReservationTaskStatus taskStatus = queueParticipation
                ? ReservationTaskStatus.PENDING_CONFIRM
                : ReservationTaskStatus.COMPLETED;
        LocalDateTime completedAt = taskStatus == ReservationTaskStatus.COMPLETED ? now : null;
        TaskCreation taskCreation = insertTaskWithQueueRetry(
                hotelId,
                sourceMessageId,
                transitionId,
                resultType,
                aiTaskType,
                mapping,
                taskSubtype,
                taskStatus.name(),
                queueParticipation,
                integerAt(item, "parent_source_event_index"),
                linkedTaskGroupId,
                booleanAt(item, "blocked_until_parent_completed"),
                completedAt,
                order,
                now
        );
        workflowRepository.insertTaskCard(new ReservationTaskCardDraft(
                hotelId,
                taskCreation.taskId(),
                mapping.taskCardType().name(),
                FIELD_CONTRACT_VERSION,
                itemPayloadJson,
                initialReviewStatus(resultType, mapping),
                now
        ));

        return new SuperAgentTaskResultItemResponse(
                sourceEventIndex,
                arrayIndex,
                transitionId.toString(),
                routeCode,
                systemProcessCategory,
                null,
                order.id().toString(),
                taskCreation.taskId().toString(),
                mapping.systemTaskType().name(),
                mapping.taskCardType().name(),
                taskStatus.name(),
                order.orderStatus(),
                taskCreation.executionOrder()
        );
    }

    /**
     * 插入任务并处理并发队列序号冲突。数据库唯一约束兜底后，这里重新取号重试。
     */
    private TaskCreation insertTaskWithQueueRetry(
            String hotelId,
            Long sourceMessageId,
            Long transitionId,
            String resultType,
            String aiTaskType,
            TaskTypeMapping mapping,
            String taskSubtype,
            String taskStatus,
            boolean queueParticipation,
            Integer parentSourceEventIndex,
            String linkedTaskGroupId,
            Boolean blockedUntilParentCompleted,
            LocalDateTime completedAt,
            ReservationOrderSnapshot order,
            LocalDateTime now) {
        for (int attempt = 1; attempt <= MAX_QUEUE_ORDER_RETRY; attempt++) {
            int executionOrder = workflowRepository.nextExecutionOrder(hotelId, order.id());
            try {
                Long taskId = workflowRepository.insertTask(new ReservationTaskDraft(
                        hotelId,
                        order.id(),
                        sourceMessageId,
                        transitionId,
                        resultType,
                        aiTaskType,
                        mapping.systemTaskType().name(),
                        mapping.taskCardType().name(),
                        taskSubtype,
                        taskStatus,
                        queueParticipation,
                        executionOrder,
                        parentSourceEventIndex,
                        linkedTaskGroupId,
                        blockedUntilParentCompleted,
                        completedAt,
                        now
                ));
                return new TaskCreation(taskId, executionOrder);
            } catch (DuplicateKeyException exception) {
                if (attempt == MAX_QUEUE_ORDER_RETRY) {
                    throw error(HttpStatus.CONFLICT, "TASK_QUEUE_ORDER_CONFLICT", "同订单任务队列序号并发写入冲突。");
                }
            }
        }
        throw error(HttpStatus.CONFLICT, "TASK_QUEUE_ORDER_CONFLICT", "同订单任务队列序号并发写入冲突。");
    }

    /**
     * 根据系统主任务类型和候选业务号解析订单归属；CP1-3 只做自动匹配和临时订单。
     */
    private ReservationOrderSnapshot resolveOrder(
            String hotelId,
            Long sourceMessageId,
            ReservationSystemTaskType systemTaskType,
            String groupCode,
            String confirmationNumber,
            int arrayIndex,
            LocalDateTime now) {
        if (systemTaskType == ReservationSystemTaskType.MANUAL_REVIEW
                || systemTaskType == ReservationSystemTaskType.INFORMATIONAL_MESSAGE) {
            return createTemporaryOrder(hotelId, sourceMessageId, arrayIndex, now);
        }
        BusinessKey businessKey = selectBusinessKey(groupCode, confirmationNumber);
        if (businessKey == null) {
            return createTemporaryOrder(hotelId, sourceMessageId, arrayIndex, now);
        }
        return workflowRepository.findActiveOrderByBusinessKey(hotelId, businessKey.keyType().name(), businessKey.value())
                .orElseGet(() -> createActiveOrder(hotelId, sourceMessageId, businessKey, arrayIndex, now));
    }

    /**
     * 选择订单业务号候选，Group Code 优先于 Confirmation Number。
     */
    private BusinessKey selectBusinessKey(String groupCode, String confirmationNumber) {
        if (hasText(groupCode)) {
            return new BusinessKey(ReservationOrderKeyType.GROUP_CODE, groupCode);
        }
        if (hasText(confirmationNumber)) {
            return new BusinessKey(ReservationOrderKeyType.CONFIRMATION_NUMBER, confirmationNumber);
        }
        return null;
    }

    /**
     * 创建带业务号的有效订单。
     */
    private ReservationOrderSnapshot createActiveOrder(
            String hotelId,
            Long sourceMessageId,
            BusinessKey businessKey,
            int arrayIndex,
            LocalDateTime now) {
        try {
            return workflowRepository.insertOrder(new ReservationOrderDraft(
                    hotelId,
                    businessKey.keyType().name(),
                    businessKey.value(),
                    businessKey.value(),
                    temporaryOrderCode(sourceMessageId, arrayIndex),
                    ReservationOrderStatus.ACTIVE.name(),
                    ReservationOrderVisibility.VISIBLE.name(),
                    ReservationBusinessKeySource.AI_CANDIDATE.name(),
                    null,
                    businessKey.value(),
                    sourceMessageId,
                    now
            ));
        } catch (DuplicateKeyException exception) {
            return workflowRepository
                    .findActiveOrderByBusinessKey(hotelId, businessKey.keyType().name(), businessKey.value())
                    .orElseThrow(() -> error(HttpStatus.CONFLICT, "ORDER_ACTIVE_KEY_CONFLICT", "ACTIVE 订单业务号并发写入冲突。"));
        }
    }

    /**
     * 创建临时订单，用作无业务号任务、人工复核和信息提醒的归档容器。
     */
    private ReservationOrderSnapshot createTemporaryOrder(
            String hotelId,
            Long sourceMessageId,
            int arrayIndex,
            LocalDateTime now) {
        String temporaryCode = temporaryOrderCode(sourceMessageId, arrayIndex);
        return workflowRepository.insertOrder(new ReservationOrderDraft(
                hotelId,
                ReservationOrderKeyType.TEMPORARY.name(),
                null,
                null,
                temporaryCode,
                ReservationOrderStatus.TEMPORARY.name(),
                ReservationOrderVisibility.VISIBLE.name(),
                null,
                null,
                temporaryCode,
                sourceMessageId,
                now
        ));
    }

    /**
     * 创建 S000/S999 专用隐藏技术订单。它只满足任务外键归属，不进入前端订单列表。
     */
    private ReservationOrderSnapshot createHiddenSourceMessageOnlyOrder(
            String hotelId,
            Long sourceMessageId,
            LocalDateTime now) {
        String temporaryCode = temporaryOrderCode(sourceMessageId, 1);
        return workflowRepository.insertOrder(new ReservationOrderDraft(
                hotelId,
                ReservationOrderKeyType.TEMPORARY.name(),
                null,
                null,
                temporaryCode,
                ReservationOrderStatus.TEMPORARY.name(),
                ReservationOrderVisibility.HIDDEN_SYSTEM.name(),
                null,
                null,
                temporaryCode,
                sourceMessageId,
                now
        ));
    }

    /**
     * 映射 AI 原始任务类型到系统主任务类型和任务卡类型。
     */
    private TaskTypeMapping mapTaskType(String resultType, String aiTaskType, String taskSubtype) {
        ReservationAiRouteDefinition route = ReservationAiRouteDefinition
                .findByTriplet(resultType, aiTaskType, taskSubtype)
                .orElse(null);
        if (route != null && route.businessTask()) {
            return new TaskTypeMapping(route.systemTaskType(), route.taskCardType());
        }
        if (AiResultType.MANUAL_REVIEW.code().equals(resultType) || "Fallback".equals(aiTaskType)) {
            return new TaskTypeMapping(
                    ReservationSystemTaskType.MANUAL_REVIEW,
                    ReservationTaskCardType.FALLBACK_REVIEW);
        }
        if (AiResultType.INFORMATIONAL_MESSAGE.code().equals(resultType) || "Message Notification".equals(aiTaskType)) {
            return new TaskTypeMapping(
                    ReservationSystemTaskType.INFORMATIONAL_MESSAGE,
                    ReservationTaskCardType.MESSAGE_NOTIFICATION);
        }
        if (!AiResultType.NORMAL_TASK.code().equals(resultType)) {
            throw error(HttpStatus.BAD_REQUEST, "TASK_RESULT_UNSUPPORTED_TYPE", "result_type 不可识别。");
        }
        return switch (aiTaskType) {
            case "New Booking" -> new TaskTypeMapping(
                    ReservationSystemTaskType.NEW_BOOKING,
                    ReservationTaskCardType.NEW_BOOKING);
            case "Cancel Booking" -> new TaskTypeMapping(
                    ReservationSystemTaskType.CANCEL_BOOKING,
                    ReservationTaskCardType.CANCEL_BOOKING);
            case "Cancel Allotment" -> new TaskTypeMapping(
                    ReservationSystemTaskType.CANCEL_BOOKING,
                    ReservationTaskCardType.CANCEL_ALLOTMENT);
            case "Update Booking" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.UPDATE_BOOKING);
            case "Voucher Received" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.VOUCHER_RECEIVED);
            case "Payment Evidence" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.PAYMENT_EVIDENCE);
            case "Rooming List" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.ROOMING_LIST);
            case "Amend Group Code" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.AMEND_GROUP_CODE);
            case "Trace / Reservation Notes" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.TRACE_RESERVATION_NOTES);
            case "TA Recorder" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.TA_RECORDER);
            case "Invoice Generation" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.INVOICE_GENERATION);
            case "Invoice Received" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.INVOICE_RECEIVED);
            case "Payment Notice" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.PAYMENT_NOTICE);
            case "Manual RateCode" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.MANUAL_RATE_CODE);
            default -> throw error(HttpStatus.BAD_REQUEST, "TASK_RESULT_UNSUPPORTED_TYPE", "task_type 不可识别。");
        };
    }

    /**
     * 仅 type-known manual_review 业务任务需要在原任务卡上进入待复核状态，Fallback 转换任务继续走原人工转换流程。
     */
    private String initialReviewStatus(String resultType, TaskTypeMapping mapping) {
        if (!AiResultType.MANUAL_REVIEW.code().equals(resultType)) {
            return null;
        }
        return mapping.systemTaskType() == ReservationSystemTaskType.MANUAL_REVIEW
                ? null
                : ReservationReviewStatus.PENDING.name();
    }

    /**
     * 解析 SuperAgent 入口阶段的纯文本结果。兼容旧 S000/S999，也允许调试期文本 S10/S99。
     */
    private SourceMessageOnlyEntryResult parseSourceMessageOnlyEntryResult(String rawBody) {
        String trimmedBody = trimToNull(rawBody);
        if (trimmedBody == null) {
            return null;
        }
        int separatorIndex = trimmedBody.indexOf(',');
        String code = separatorIndex < 0 ? trimmedBody : trimmedBody.substring(0, separatorIndex).trim();
        SourceMessageOnlyResultCode resultCode = SourceMessageOnlyResultCode.fromCode(code).orElse(null);
        if (resultCode == null) {
            return null;
        }
        if (separatorIndex < 0) {
            throw error(HttpStatus.BAD_REQUEST, "SOURCE_MESSAGE_REQUIRED", "入口结果缺少 source_message_id。");
        }
        String externalSourceMessageId = trimToNull(trimmedBody.substring(separatorIndex + 1));
        if (externalSourceMessageId == null) {
            throw error(HttpStatus.BAD_REQUEST, "SOURCE_MESSAGE_REQUIRED", "入口结果缺少 source_message_id。");
        }
        validateLength(externalSourceMessageId, "source_message_id", LENGTH_256);
        return new SourceMessageOnlyEntryResult(
                resultCode,
                externalSourceMessageId,
                SOURCE_MESSAGE_ONLY_RESULT_TYPE,
                resultCode.code());
    }

    /**
     * 解析请求体 JSON，解析失败返回受控错误，不输出原始请求体。
     */
    private JsonNode parseJson(String rawBody) {
        try {
            return objectMapper.readTree(rawBody == null ? "" : rawBody);
        } catch (Exception exception) {
            throw error(HttpStatus.BAD_REQUEST, "INVALID_JSON", "JSON 不可解析。");
        }
    }

    /**
     * 解析 SuperAgent 来源消息引用。正式契约使用外部邮件 ID，旧本地夹具仍兼容内部 SourceMessage ID。
     */
    private ResolvedSourceMessage resolveSourceMessage(JsonNode root, String defaultHotelId) {
        String sourceMessageReference = trimToNull(textAt(root, "source_message_id"));
        if (sourceMessageReference == null) {
            throw error(HttpStatus.BAD_REQUEST, "SOURCE_MESSAGE_REQUIRED", "source_message_id 缺失。");
        }
        validateLength(sourceMessageReference, "source_message_id", LENGTH_256);

        String requestHotelId = trimToNull(textAt(root, "hotel_id"));
        String systemHotelId = trimToNull(defaultHotelId);
        if (requestHotelId != null && systemHotelId != null && !requestHotelId.equals(systemHotelId)) {
            throw error(HttpStatus.BAD_REQUEST, "HOTEL_ID_MISMATCH", "请求 hotel_id 与系统酒店不一致。");
        }
        String hotelId = requestHotelId == null ? systemHotelId : requestHotelId;
        if (hotelId == null) {
            if (allowLegacyInternalSourceMessageId) {
                return resolveLegacyInternalSourceMessage(sourceMessageReference, null);
            }
            throw error(HttpStatus.BAD_REQUEST, "HOTEL_ID_REQUIRED", "使用外部 source_message_id 时 hotel_id 不能为空。");
        }
        validateLength(hotelId, "hotel_id", LENGTH_64);
        // 旧 V2 字段只做格式兼容校验；写入定位统一按系统酒店 + external_message_id 匹配真实 Inbox。
        optionalText(textAt(root, "source_provider"), "source_provider", LENGTH_32);
        optionalText(textAt(root, "source_channel"), "source_channel", LENGTH_32);
        return resolveExternalSourceMessageReferenceByHotel(
                hotelId,
                sourceMessageReference,
                requestHotelId == null && allowLegacyInternalSourceMessageId);
    }

    /**
     * 兼容历史本地测试和旧接口调用：无 hotel_id 时按内部 SourceMessage ID 查询，但必须校验系统酒店边界。
     */
    private ResolvedSourceMessage resolveLegacyInternalSourceMessage(String rawId, String expectedHotelId) {
        Long sourceMessageId;
        try {
            sourceMessageId = Long.valueOf(rawId);
        } catch (NumberFormatException exception) {
            throw error(HttpStatus.BAD_REQUEST, "HOTEL_ID_REQUIRED", "使用外部 source_message_id 时 hotel_id 不能为空。");
        }
        SourceMessageInboxSnapshot sourceMessage = sourceMessageInboxRepository.findById(sourceMessageId)
                .orElse(null);
        if (sourceMessage == null) {
            LOGGER.warn(
                    "SuperAgent 任务结果未匹配 SourceMessage：lookupMode=legacy_internal_id, "
                            + "sourceMessageId={}, expectedHotelId={}",
                    rawId,
                    expectedHotelId);
            throw error(HttpStatus.NOT_FOUND, "SOURCE_MESSAGE_NOT_FOUND", "SourceMessage 不存在。");
        }
        if (expectedHotelId != null && !expectedHotelId.equals(sourceMessage.hotelId())) {
            throw error(HttpStatus.BAD_REQUEST, "HOTEL_ID_MISMATCH", "SourceMessage 所属酒店与系统酒店不一致。");
        }
        String responseSourceMessageId = trimToNull(sourceMessage.externalMessageId()) == null
                ? sourceMessageId.toString()
                : sourceMessage.externalMessageId();
        return new ResolvedSourceMessage(sourceMessage, responseSourceMessageId);
    }

    /**
     * 按系统酒店和 AgentBus 外部邮件 ID 解析真实入库 SourceMessage，不要求 SuperAgent 了解数据库 provider/channel。
     */
    private SourceMessageInboxSnapshot resolveExternalSourceMessageByHotel(String hotelId, String externalSourceMessageId) {
        List<SourceMessageInboxSnapshot> candidates =
                sourceMessageInboxRepository.findByExternalMessageId(hotelId, externalSourceMessageId);
        if (candidates.isEmpty()) {
            LOGGER.warn(
                    "SuperAgent 入口结果未匹配 SourceMessage：lookupMode=external_message_id, "
                            + "hotelId={}, sourceMessageId={}",
                    hotelId,
                    externalSourceMessageId);
            throw error(HttpStatus.NOT_FOUND, "SOURCE_MESSAGE_NOT_FOUND", "SourceMessage 不存在。");
        }
        if (candidates.size() > 1) {
            throw error(HttpStatus.CONFLICT, "SOURCE_MESSAGE_AMBIGUOUS", "同一酒店下存在多个匹配的 SourceMessage。");
        }
        return candidates.get(0);
    }

    /**
     * V2 写入优先按外部消息 ID 解析；仅在无显式 hotel_id 的旧本地夹具场景下兜底内部 SourceMessage ID。
     */
    private ResolvedSourceMessage resolveExternalSourceMessageReferenceByHotel(
            String hotelId,
            String sourceMessageReference,
            boolean allowLegacyInternalFallback) {
        List<SourceMessageInboxSnapshot> candidates =
                sourceMessageInboxRepository.findByExternalMessageId(hotelId, sourceMessageReference);
        if (candidates.isEmpty()) {
            if (allowLegacyInternalFallback && isLongText(sourceMessageReference)) {
                return resolveLegacyInternalSourceMessage(sourceMessageReference, hotelId);
            }
            LOGGER.warn(
                    "SuperAgent 任务结果未匹配 SourceMessage：lookupMode=external_message_id, "
                            + "hotelId={}, sourceMessageId={}, legacyFallbackAllowed={}",
                    hotelId,
                    sourceMessageReference,
                    allowLegacyInternalFallback);
            throw error(HttpStatus.NOT_FOUND, "SOURCE_MESSAGE_NOT_FOUND", "SourceMessage 不存在。");
        }
        if (candidates.size() > 1) {
            throw error(HttpStatus.CONFLICT, "SOURCE_MESSAGE_AMBIGUOUS", "同一酒店下存在多个匹配的 SourceMessage。");
        }
        return new ResolvedSourceMessage(candidates.get(0), sourceMessageReference);
    }

    /**
     * 判断来源消息引用是否为历史本地兼容的内部 SourceMessage ID。
     */
    private boolean isLongText(String value) {
        try {
            Long.valueOf(value);
            return true;
        } catch (NumberFormatException exception) {
            return false;
        }
    }

    /**
     * 从对象节点中读取文本字段，缺失或 null 时返回 null。
     */
    private String textAt(JsonNode node, String fieldName) {
        if (node == null || node.isMissingNode() || node.get(fieldName) == null || node.get(fieldName).isNull()) {
            return null;
        }
        return node.get(fieldName).asText();
    }

    /**
     * 读取正整数，AI 事件序号必须存在且大于 0。
     */
    private Integer requirePositiveInt(JsonNode node, String fieldName) {
        Integer value = integerAt(node, fieldName);
        if (value == null || value <= 0) {
            throw error(HttpStatus.BAD_REQUEST, "TASK_RESULT_UNSUPPORTED_TYPE", fieldName + " 无效。");
        }
        return value;
    }

    /**
     * 读取可选整数字段。
     */
    private Integer integerAt(JsonNode node, String fieldName) {
        if (node == null || node.get(fieldName) == null || node.get(fieldName).isNull()) {
            return null;
        }
        return node.get(fieldName).asInt();
    }

    /**
     * 读取可选布尔字段。
     */
    private Boolean booleanAt(JsonNode node, String fieldName) {
        if (node == null || node.get(fieldName) == null || node.get(fieldName).isNull()) {
            return Boolean.FALSE;
        }
        return node.get(fieldName).asBoolean(false);
    }

    /**
     * 判断 JSON 节点是否为布尔 true。S10/S99 P0 契约不接受字符串 true 这类宽松输入。
     */
    private boolean isBooleanTrue(JsonNode node) {
        return node != null && node.isBoolean() && node.booleanValue();
    }

    /**
     * 将 JSON 节点序列化为字符串，缺失时返回 null。
     */
    private String nodeJson(JsonNode node) {
        if (node == null || node.isMissingNode() || node.isNull()) {
            return null;
        }
        try {
            return objectMapper.writeValueAsString(node);
        } catch (Exception exception) {
            throw new IllegalStateException("JSON 节点序列化失败", exception);
        }
    }

    /**
     * 计算 SHA-256，用于批次和 item 幂等键。
     */
    private String sha256(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException exception) {
            throw new IllegalStateException("当前 Java 运行时不支持 SHA-256", exception);
        }
    }

    /**
     * 生成临时订单编号，保证同一 SourceMessage 多 item 不冲突。
     */
    private String temporaryOrderCode(Long sourceMessageId, int arrayIndex) {
        return "TMP-SM-" + sourceMessageId + "-" + arrayIndex;
    }

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

    /**
     * 校验必填文本字段。
     */
    private String requireText(String value, String fieldName, int maxLength) {
        String trimmed = trimToNull(value);
        if (trimmed == null) {
            throw error(HttpStatus.BAD_REQUEST, "TASK_RESULT_UNSUPPORTED_TYPE", fieldName + " 缺失。");
        }
        validateLength(trimmed, fieldName, maxLength);
        return trimmed;
    }

    /**
     * 校验可选文本字段长度，防止外部输入直接触发数据库字段超长异常。
     */
    private String optionalText(String value, String fieldName, int maxLength) {
        String trimmed = trimToNull(value);
        if (trimmed == null) {
            return null;
        }
        validateLength(trimmed, fieldName, maxLength);
        return trimmed;
    }

    /**
     * 校验稳定字段长度，与第一版表结构保持一致。
     */
    private void validateLength(String value, String fieldName, int maxLength) {
        if (value.length() > maxLength) {
            throw error(HttpStatus.BAD_REQUEST, "TASK_RESULT_FIELD_TOO_LONG", fieldName + " 超过长度限制。");
        }
    }

    /**
     * 判断是否为 V3 基础设施输入错误。该结果不建 SourceMessage、批次、任务或通知卡。
     */
    private boolean isInfrastructureInputError(JsonNode root) {
        return "infrastructure_input_error".equals(textAt(root, "result_type"));
    }

    /**
     * 判断是否为 P0 明确约定的缺失 source_message_id 基础设施错误；其他 infra 错误不得被改写成该错误。
     */
    private boolean isMissingSourceMessageInfrastructureInputError(JsonNode root) {
        return isInfrastructureInputError(root)
                && "missing_source_message_id".equals(textAt(root, "error_code"));
    }

    /**
     * V3 或入口网关形态输入必须先有非空 source_message_id；纯 V2 兼容结构不受该预检查影响。
     */
    private boolean isV3SourceMessageIdentityMissing(JsonNode root) {
        if (root == null || !isV3OrGatewayInputShape(root)) {
            return false;
        }
        JsonNode sourceMessage = root.get("source_message");
        return sourceMessage == null
                || !sourceMessage.isObject()
                || trimToNull(textAt(sourceMessage, "source_message_id")) == null;
    }

    /**
     * 构建 0711 P0 基础设施输入错误，ControllerAdvice 会转换为扁平 typed 响应。
     */
    private ReservationAiTaskIntakeException infrastructureInputError() {
        return error(HttpStatus.BAD_REQUEST, "MISSING_SOURCE_MESSAGE_ID", "source_message.source_message_id 缺失。");
    }

    /**
     * 判断输入是否属于 V3 回调或 0711 P0 入口网关形态，避免误伤旧 V2 兼容 JSON。
     */
    private boolean isV3OrGatewayInputShape(JsonNode root) {
        return root.path("message_events").isArray()
                || root.path("unhandled_current_intents").isArray()
                || root.path("candidate_events").isArray()
                || root.has("body_current")
                || isV3SourceMessageNotification(root);
    }

    /**
     * 判断是否为 V3 结构化 S10/S99 来源邮件通知根。
     */
    private boolean isV3SourceMessageNotification(JsonNode root) {
        String routeCode = trimToNull(textAt(root, "route_code"));
        return AiResultType.SOURCE_MESSAGE_REVIEW_NOTIFICATION.code().equals(textAt(root, "result_type"))
                || "S10".equals(routeCode)
                || "S99".equals(routeCode);
    }

    /**
     * 判断是否为 V3 业务根，业务根必须携带 source_message 和 message_events 数组。
     */
    private boolean isV3BusinessRoot(JsonNode root) {
        return root != null
                && root.has("source_message")
                && root.path("message_events").isArray();
    }

    /**
     * 解析 V3 根对象中的 SourceMessage 外部 ID，并按系统默认酒店反查 Inbox 内部记录。
     */
    private ResolvedSourceMessage resolveV3SourceMessage(JsonNode root, String defaultHotelId) {
        JsonNode sourceMessageNode = root.path("source_message");
        String externalSourceMessageId = trimToNull(textAt(sourceMessageNode, "source_message_id"));
        if (externalSourceMessageId == null) {
            throw infrastructureInputError();
        }
        validateLength(externalSourceMessageId, "source_message.source_message_id", LENGTH_256);
        String hotelId = requireText(defaultHotelId, "default_hotel_id", LENGTH_64);
        SourceMessageInboxSnapshot sourceMessage =
                resolveExternalSourceMessageByHotel(hotelId, externalSourceMessageId);
        return new ResolvedSourceMessage(sourceMessage, externalSourceMessageId);
    }

    /**
     * 保存 V3 event 的 Adapter 契约错误或未处理意图，只落 AI transition，不创建订单和业务任务。
     */
    private SuperAgentTaskResultItemResponse createAdapterContractErrorTransition(
            String hotelId,
            Long sourceMessageId,
            Long batchId,
            JsonNode event,
            int arrayIndex,
            int sourceEventIndex,
            ReservationAiRouteDefinition route,
            String adapterErrorCode,
            String adapterErrorMessage,
            LocalDateTime now) {
        JsonNode caseKeys = event == null ? null : event.path("case_keys");
        JsonNode manualReview = event == null ? null : event.path("manual_review");
        String resultType = route == null ? AiResultType.ADAPTER_CONTRACT_ERROR.code() : route.resultType();
        String aiTaskType = route == null
                ? optionalTransitionSummary(textAt(event, "event_type"), LENGTH_64)
                : route.taskType();
        if (aiTaskType == null) {
            aiTaskType = ADAPTER_CONTRACT_ERROR_CODE;
        }
        String taskSubtype = route == null ? null : route.taskSubtype();
        String routeCode = route == null ? null : route.routeCode();
        String systemProcessCategory = route == null
                ? ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR.name()
                : route.systemProcessCategory().name();
        String systemTaskType = route == null
                ? ReservationSystemTaskType.ADAPTER_CONTRACT_ERROR.name()
                : route.systemTaskType().name();
        String taskCardType = route == null
                ? ReservationTaskCardType.ADAPTER_CONTRACT_ERROR.name()
                : route.taskCardType().name();
        String itemPayloadJson = nodeJson(event);
        if (itemPayloadJson == null) {
            itemPayloadJson = "{}";
        }
        String itemPayloadSha256 = sha256(itemPayloadJson);
        String safeAdapterErrorCode = route == null
                ? requireText(adapterErrorCode, "adapter_error_code", LENGTH_128)
                : optionalTransitionSummary(adapterErrorCode, LENGTH_128);
        String safeAdapterErrorMessage = optionalTransitionSummary(adapterErrorMessage, LENGTH_512);
        String itemIdempotencyKey = sha256(ITEM_KEY_PREFIX
                + "|" + sourceMessageId
                + "|" + sourceEventIndex
                + "|" + arrayIndex
                + "|" + V3_CATALOG_CODE
                + "|" + V3_SKILL_ID
                + "|" + resultType
                + "|" + aiTaskType
                + "|" + nullToEmpty(taskSubtype)
                + "|" + safeAdapterErrorCode
                + "|" + itemPayloadSha256);
        Long transitionId = workflowRepository.insertTransition(new ReservationAiTransitionDraft(
                hotelId,
                batchId,
                sourceMessageId,
                sourceEventIndex,
                arrayIndex,
                arrayIndex,
                V3_CATALOG_CODE,
                V3_SKILL_ID,
                resultType,
                aiTaskType,
                routeCode,
                systemProcessCategory,
                systemTaskType,
                taskCardType,
                taskSubtype,
                optionalTransitionSummary(textAt(event, "current_or_history"), LENGTH_32),
                optionalTransitionSummary(textAt(caseKeys, "group_code"), LENGTH_128),
                optionalTransitionSummary(textAt(caseKeys, "confirmation_number"), LENGTH_128),
                itemPayloadSha256,
                itemIdempotencyKey,
                optionalTransitionSummary(textAt(manualReview, "reason_code"), LENGTH_128),
                integerAt(event, "parent_source_event_index"),
                optionalTransitionSummary(textAt(event, "linked_task_group_id"), LENGTH_128),
                booleanAt(event, "blocked_until_parent_completed"),
                itemPayloadJson,
                nodeJson(event == null ? null : event.get("case_keys")),
                nodeJson(event == null ? null : event.get("extracted_fields")),
                nodeJson(event == null ? null : event.get("manual_review")),
                null,
                nodeJson(event == null ? null : event.get("attachments")),
                nodeJson(event == null ? null : event.get("context_used")),
                safeAdapterErrorCode,
                safeAdapterErrorMessage,
                now
        ));
        return new SuperAgentTaskResultItemResponse(
                sourceEventIndex,
                arrayIndex,
                transitionId.toString(),
                routeCode,
                systemProcessCategory,
                safeAdapterErrorCode,
                null,
                null,
                systemTaskType,
                taskCardType,
                null,
                null,
                null);
    }

    /**
     * 将 V3 source_event_index 归一为数字；E1、"1" 和整数 1 都会转为 1。
     */
    private int sourceEventIndex(JsonNode event, int fallback) {
        JsonNode indexNode = event == null ? null : event.get("source_event_index");
        if (indexNode == null || indexNode.isNull()) {
            return fallback;
        }
        if (indexNode.isInt() || indexNode.isLong()) {
            int value = indexNode.asInt();
            return value > 0 ? value : fallback;
        }
        String rawValue = trimToNull(indexNode.asText());
        if (rawValue == null) {
            return fallback;
        }
        String numeric = rawValue.startsWith("E") || rawValue.startsWith("e")
                ? rawValue.substring(1)
                : rawValue;
        try {
            int value = Integer.parseInt(numeric);
            return value > 0 ? value : fallback;
        } catch (NumberFormatException exception) {
            return fallback;
        }
    }

    /**
     * 复制 V3 event 字段到归一化 item，缺失字段不写入。
     */
    private void copyIfPresent(JsonNode source, ObjectNode target, String fieldName) {
        if (source == null || target == null || fieldName == null) {
            return;
        }
        JsonNode value = source.get(fieldName);
        if (value != null && !value.isNull()) {
            target.set(fieldName, value);
        }
    }

    /**
     * 判断 JSON 节点是否为空、缺失或 null。
     */
    private boolean isNullOrMissing(JsonNode node) {
        return node == null || node.isMissingNode() || node.isNull();
    }

    /**
     * 优先从主对象读取字段，读取不到再从备用对象读取。
     */
    private String firstText(JsonNode primary, JsonNode fallback, String fieldName) {
        String value = trimToNull(textAt(primary, fieldName));
        return value == null ? trimToNull(textAt(fallback, fieldName)) : value;
    }

    /**
     * Adapter 契约错误路径只保存安全摘要，外部长字段不能反向导致整批回滚。
     */
    private String optionalTransitionSummary(String value, int maxLength) {
        String trimmed = trimToNull(value);
        if (trimmed == null) {
            return null;
        }
        return trimmed.length() > maxLength ? trimmed.substring(0, maxLength) : trimmed;
    }

    /**
     * 判断数组字段是否存在且非空。
     */
    private boolean nonEmptyArray(JsonNode node, String fieldName) {
        JsonNode value = node == null ? null : node.path(fieldName);
        return value != null && value.isArray() && value.size() > 0;
    }

    /**
     * 校验对象字段集合必须与 P0 契约完全一致。
     */
    private boolean hasExactFields(JsonNode node, List<String> expectedFields) {
        if (node == null || !node.isObject() || node.size() != expectedFields.size()) {
            return false;
        }
        for (String fieldName : expectedFields) {
            if (!node.has(fieldName)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 校验对象必须包含指定字段；允许 P0 event 的关系扩展字段继续保留。
     */
    private boolean hasRequiredFields(JsonNode node, List<String> requiredFields) {
        if (node == null || !node.isObject()) {
            return false;
        }
        for (String fieldName : requiredFields) {
            if (!node.has(fieldName)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 判断字段值是否为字符串或 null。
     */
    private boolean isStringOrNull(JsonNode node) {
        return node != null && (node.isNull() || node.isTextual());
    }

    /**
     * 判断字段值是否为字符串数组。
     */
    private boolean isStringArray(JsonNode node, boolean nonempty) {
        if (node == null || !node.isArray() || (nonempty && node.isEmpty())) {
            return false;
        }
        for (JsonNode item : node) {
            if (!item.isTextual()) {
                return false;
            }
        }
        return true;
    }

    /**
     * 读取非空字符串数组；数组不存在或包含非字符串时返回空集合，交由调用方 fail closed。
     */
    private List<String> stringArrayValues(JsonNode node) {
        if (node == null || !node.isArray()) {
            return List.of();
        }
        List<String> values = new ArrayList<>(node.size());
        for (JsonNode item : node) {
            String value = item.isTextual() ? trimToNull(item.asText()) : null;
            if (value == null) {
                return List.of();
            }
            values.add(value);
        }
        return values;
    }

    /**
     * 按 source_event_index 在同一 V3 根的 message_events 中查找事件。
     */
    private JsonNode findV3MessageEventBySourceEventIndex(JsonNode messageEvents, String sourceEventIndex) {
        if (messageEvents == null || !messageEvents.isArray() || trimToNull(sourceEventIndex) == null) {
            return null;
        }
        for (JsonNode candidate : messageEvents) {
            if (sourceEventIndex.equals(trimToNull(textAt(candidate, "source_event_index")))) {
                return candidate;
            }
        }
        return null;
    }

    /**
     * 判断 RFC 6901 JSON Pointer 是否能定位到指定文档节点，null 值也视为有效目标。
     */
    private boolean jsonPointerTargetExists(JsonNode document, String pointer) {
        List<String> tokens = jsonPointerTokens(pointer);
        if (tokens == null) {
            return false;
        }
        JsonNode current = document;
        for (String token : tokens) {
            if (current == null || current.isMissingNode()) {
                return false;
            }
            if (current.isObject()) {
                if (!current.has(token)) {
                    return false;
                }
                current = current.get(token);
                continue;
            }
            if (current.isArray()) {
                Integer arrayIndex = jsonPointerArrayIndex(token);
                if (arrayIndex == null || arrayIndex >= current.size()) {
                    return false;
                }
                current = current.get(arrayIndex);
                continue;
            }
            return false;
        }
        return current != null && !current.isMissingNode();
    }

    /**
     * 解析 RFC 6901 JSON Pointer token，拒绝非法 ~ 转义。
     */
    private List<String> jsonPointerTokens(String pointer) {
        String safePointer = trimToNull(pointer);
        if (safePointer == null || !safePointer.startsWith("/")) {
            return null;
        }
        String[] rawTokens = safePointer.substring(1).split("/", -1);
        List<String> tokens = new ArrayList<>(rawTokens.length);
        for (String rawToken : rawTokens) {
            String token = jsonPointerToken(rawToken);
            if (token == null) {
                return null;
            }
            tokens.add(token);
        }
        return tokens;
    }

    /**
     * 解析单个 RFC 6901 token。
     */
    private String jsonPointerToken(String rawToken) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < rawToken.length(); i++) {
            char current = rawToken.charAt(i);
            if (current != '~') {
                builder.append(current);
                continue;
            }
            if (i + 1 >= rawToken.length()) {
                return null;
            }
            char escaped = rawToken.charAt(++i);
            if (escaped == '0') {
                builder.append('~');
            } else if (escaped == '1') {
                builder.append('/');
            } else {
                return null;
            }
        }
        return builder.toString();
    }

    /**
     * 解析 JSON Pointer 数组下标，负数和非数字不接受。
     */
    private Integer jsonPointerArrayIndex(String token) {
        if (token == null || token.isEmpty()) {
            return null;
        }
        for (int i = 0; i < token.length(); i++) {
            if (!Character.isDigit(token.charAt(i))) {
                return null;
            }
        }
        try {
            return Integer.valueOf(token);
        } catch (NumberFormatException exception) {
            return null;
        }
    }

    /**
     * 判断文本是否有效。
     */
    private boolean hasText(String value) {
        return value != null && !value.trim().isEmpty();
    }

    /**
     * 已解析的来源消息。snapshot 使用内部 ID 参与落库，responseSourceMessageId 面向 SuperAgent 回显外部 ID。
     */
    private record ResolvedSourceMessage(
            SourceMessageInboxSnapshot snapshot,
            String responseSourceMessageId
    ) {
    }

    /**
     * V3 单个 event 的契约问题摘要。只保存稳定 code 和安全说明，不保存客户原文。
     */
    private record V3EventContractIssue(
            String code,
            String message
    ) {
    }

    /**
     * Parent split 父事件身份归一结果。target_object_unclear 代表已知卡型但目标对象需人工确认。
     */
    private record ParentSplitIdentity(
            String groupCode,
            String blockCode,
            String normalizedParentCode,
            boolean targetObjectUnclearReview
    ) {
    }

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

    /**
     * null 转为空字符串，保证幂等键拼接稳定。
     */
    private String nullToEmpty(String value) {
        return value == null ? "" : value;
    }

    /**
     * 构建受控错误。
     */
    private ReservationAiTaskIntakeException error(HttpStatus status, String errorCode, String message) {
        return new ReservationAiTaskIntakeException(status, errorCode, message);
    }

    /**
     * 任务类型映射结果。
     */
    private record TaskTypeMapping(
            ReservationSystemTaskType systemTaskType,
            ReservationTaskCardType taskCardType
    ) {
    }

    /**
     * 订单业务号候选。
     */
    private record BusinessKey(
            ReservationOrderKeyType keyType,
            String value
    ) {
    }

    /**
     * SuperAgent 来源邮件只读结果解析值。旧文本和 V3 结构化通知共用该载体。
     */
    private record SourceMessageOnlyEntryResult(
            SourceMessageOnlyResultCode resultCode,
            String externalSourceMessageId,
            String resultTypeCode,
            String routeCode
    ) {
    }

    /**
     * 任务创建结果，包含任务 ID 和最终写入的同订单执行序号。
     */
    private record TaskCreation(
            Long taskId,
            Integer executionOrder
    ) {
    }
}
