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.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.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.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 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.List;
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 String FIELD_CONTRACT_VERSION = "code-v1";
    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 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 MAX_QUEUE_ORDER_RETRY = 5;

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

    /**
     * 注入 JSON 解析器、SourceMessage 查询边界和 Reservation 持久化边界。
     */
    public ReservationAiTaskIntakeServiceImpl(
            ObjectMapper objectMapper,
            SourceMessageInboxRepository sourceMessageInboxRepository,
            ReservationAiWorkflowRepository workflowRepository) {
        this.objectMapper = objectMapper;
        this.sourceMessageInboxRepository = sourceMessageInboxRepository;
        this.workflowRepository = workflowRepository;
    }

    /**
     * 接收已通过 HMAC 鉴权的 SuperAgent 任务结果，创建 AI 过渡记录和最小任务模型。
     */
    @Override
    @Transactional
    public SuperAgentTaskResultResponse accept(String rawBody, String clientId, String requestId) {
        JsonNode root = parseJson(rawBody);
        ResolvedSourceMessage resolvedSourceMessage = resolveSourceMessage(root);
        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 不能为空。");
        }

        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()
        );
    }

    /**
     * 插入批次；如果并发请求已经写入同一 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_32);
        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);
        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,
                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")),
                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,
                now
        ));

        return new SuperAgentTaskResultItemResponse(
                sourceEventIndex,
                arrayIndex,
                transitionId.toString(),
                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(),
                    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(),
                null,
                null,
                temporaryCode,
                sourceMessageId,
                now
        ));
    }

    /**
     * 映射 AI 原始任务类型到系统主任务类型和任务卡类型。
     */
    private TaskTypeMapping mapTaskType(String resultType, String aiTaskType) {
        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 "Update Booking" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.UPDATE_BOOKING);
            case "Voucher Received" -> new TaskTypeMapping(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.VOUCHER_RECEIVED);
            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);
            default -> throw error(HttpStatus.BAD_REQUEST, "TASK_RESULT_UNSUPPORTED_TYPE", "task_type 不可识别。");
        };
    }

    /**
     * 解析请求体 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 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 hotelId = trimToNull(textAt(root, "hotel_id"));
        if (hotelId == null) {
            return resolveLegacyInternalSourceMessage(sourceMessageReference);
        }
        validateLength(hotelId, "hotel_id", LENGTH_64);
        String sourceProvider = optionalText(textAt(root, "source_provider"), "source_provider", LENGTH_32);
        String sourceChannel = optionalText(textAt(root, "source_channel"), "source_channel", LENGTH_32);
        String provider = sourceProvider == null ? DEFAULT_SOURCE_PROVIDER : sourceProvider;
        String channel = sourceChannel == null ? DEFAULT_SOURCE_CHANNEL : sourceChannel;
        SourceMessageInboxSnapshot sourceMessage = sourceMessageInboxRepository
                .findByIdempotencyKey(hotelId, provider, channel, sourceMessageReference)
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "SOURCE_MESSAGE_NOT_FOUND", "SourceMessage 不存在。"));
        return new ResolvedSourceMessage(sourceMessage, sourceMessageReference);
    }

    /**
     * 兼容历史本地测试和旧接口调用：无 hotel_id 时按内部 SourceMessage ID 查询。
     */
    private ResolvedSourceMessage resolveLegacyInternalSourceMessage(String rawId) {
        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)
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "SOURCE_MESSAGE_NOT_FOUND", "SourceMessage 不存在。"));
        String responseSourceMessageId = trimToNull(sourceMessage.externalMessageId()) == null
                ? sourceMessageId.toString()
                : sourceMessage.externalMessageId();
        return new ResolvedSourceMessage(sourceMessage, responseSourceMessageId);
    }

    /**
     * 从对象节点中读取文本字段，缺失或 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 节点序列化为字符串，缺失时返回 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 + " 超过长度限制。");
        }
    }

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

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

    /**
     * 将空白字符串转换为 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
    ) {
    }

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