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

import cn.nianxx.thhotel.platform.common.time.UtcTimeFormatter;
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryResponse;
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardFieldDefinition;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.enums.AiResultType;
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.ReservationOperaAttemptStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOperaOperationStatus;
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.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.request.ManualReviewConversionRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ManualReviewResolutionFieldOverrideRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ManualReviewResolutionRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOperaSimulationRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskPayloadMutationRequest;
import cn.nianxx.thhotel.workflows.reservation.common.result.ManualReviewConversionResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ManualReviewResolutionFieldOverrideResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ManualReviewResolutionResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationAiTransitionDisplayResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationAttemptResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationSourceMessageOnlyResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAuditListResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAuditLogResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAvailabilityResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskDetailResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskFieldResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskPayloadMutationResult;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
import cn.nianxx.thhotel.workflows.reservation.service.ReservationTaskCardFieldDefinitionProvider;
import cn.nianxx.thhotel.workflows.reservation.service.ReservationTaskWorkflowService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Reservation 任务工作流服务实现。负责任务详情、队列只读规则和人工复核转换。
 */
@Service
public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkflowService {

    private static final String ACTION_MANUAL_REVIEW_CONVERT = "MANUAL_REVIEW_CONVERT";
    private static final String ACTION_MANUAL_REVIEW_RESOLVE = "MANUAL_REVIEW_RESOLVE";
    private static final String ACTION_TASK_DRAFT_SAVE = "TASK_DRAFT_SAVE";
    private static final String ACTION_TASK_CONFIRM = "TASK_CONFIRM";
    private static final String ACTION_OPERA_OPERATION_EXECUTE = "OPERA_OPERATION_EXECUTE";
    private static final String ACTION_OPERA_OPERATION_RETRY = "OPERA_OPERATION_RETRY";
    private static final String ACTOR_TYPE_USER = "USER";
    private static final String ACTOR_ID_LOCAL = "local-user";
    private static final String PAYLOAD_SCHEMA_VERSION = "field_matrix-p0-room-items-v1";
    private static final String PAYLOAD_SOURCE_DRAFT = "TASK_DRAFT";
    private static final String PAYLOAD_SOURCE_CONFIRMATION = "TASK_CONFIRMATION";
    private static final String PAYLOAD_SOURCE_MANUAL_REVIEW_RESOLUTION = "MANUAL_REVIEW_RESOLUTION";
    private static final String V3_CATALOG_CODE = "M002V3";
    private static final String OPERA_OPERATION_CODE_PRECHECK = "SIMULATE_PRECHECK";
    private static final String OPERA_OPERATION_CODE_WRITE = "SIMULATE_WRITE";
    private static final int MAX_QUEUE_ORDER_RETRY = 5;

    private final ReservationAiWorkflowRepository workflowRepository;
    private final ReservationTaskCardFieldDefinitionProvider fieldDefinitionProvider;
    private final ReservationTaskAvailabilityResolver availabilityResolver;
    private final SourceMessageQueryService sourceMessageQueryService;
    private final ObjectMapper objectMapper;

    /**
     * 注入持久化边界、字段矩阵 Provider 和 JSON 解析器。
     */
    public ReservationTaskWorkflowServiceImpl(
            ReservationAiWorkflowRepository workflowRepository,
            ReservationTaskCardFieldDefinitionProvider fieldDefinitionProvider,
            ReservationTaskAvailabilityResolver availabilityResolver,
            SourceMessageQueryService sourceMessageQueryService,
            ObjectMapper objectMapper) {
        this.workflowRepository = workflowRepository;
        this.fieldDefinitionProvider = fieldDefinitionProvider;
        this.availabilityResolver = availabilityResolver;
        this.sourceMessageQueryService = sourceMessageQueryService;
        this.objectMapper = objectMapper;
    }

    /**
     * 查询任务详情，按同订单前置任务状态实时计算只读和可处理状态。
     */
    @Override
    @Transactional(readOnly = true)
    public ReservationTaskDetailResult getTaskDetail(Long taskId) {
        ReservationTaskSnapshot task = findTaskOrThrow(taskId);
        ReservationTaskCardSnapshot taskCard = workflowRepository
                .findTaskCardByTaskId(task.hotelId(), task.id())
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "TASK_CARD_NOT_FOUND", "任务卡不存在。"));
        ReservationTaskAvailabilityResult availability = availabilityResolver.calculateAvailability(task);
        List<ReservationTaskFieldResult> fields = buildFieldResults(task, taskCard);
        List<ReservationOperaOperationResult> operaOperations = findOperaOperationResults(task);
        ReservationSourceMessageOnlyResult sourceMessageOnlyResult = buildSourceMessageOnlyResult(task, taskCard);
        SourceMessageDetailContext sourceContext = findSourceMessageDetailContext(task);
        ReservationAiTransitionSnapshot transition = findTaskTransition(task);
        List<ReservationAiTransitionSnapshot> relatedTransitions = findRelatedTransitions(task, transition);
        return new ReservationTaskDetailResult(
                task.id().toString(),
                task.orderId().toString(),
                task.sourceMessageId().toString(),
                sourceContext.subject(),
                sourceContext.senderSummary(),
                sourceContext.receivedAt(),
                sourceContext.externalConversationId(),
                sourceContext.conversationMessageCount(),
                task.systemTaskType(),
                task.taskCardType(),
                task.resultType(),
                task.aiTaskType(),
                task.taskSubtype(),
                transition == null ? null : transition.routeCode(),
                transition == null ? null : transition.systemProcessCategory(),
                task.taskStatus(),
                taskCard.fieldContractVersion(),
                jsonPayloadToObject(taskCard.draftPayloadJson()),
                jsonPayloadToObject(taskCard.confirmedPayloadJson()),
                reviewStatusForDetail(task, taskCard),
                jsonPayloadToObject(taskCard.reviewResolutionJson()),
                manualReviewForDetail(task, taskCard),
                sourceMessageOnlyResult,
                availability,
                fields,
                operaOperations,
                buildTransitionDisplayResults(
                        relatedTransitions,
                        ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR),
                buildTransitionDisplayResults(
                        relatedTransitions,
                        ReservationAiSystemProcessCategory.UNHANDLED_CURRENT_INTENT));
    }

    /**
     * 将 Fallback / manual_review 任务转换为用户选择的系统任务类型，并记录审计。
     */
    @Override
    @Transactional
    public ManualReviewConversionResult convertManualReviewTask(Long taskId, ManualReviewConversionRequest request) {
        ReservationTaskSnapshot task = findTaskOrThrow(taskId);
        if (!ReservationSystemTaskType.MANUAL_REVIEW.name().equals(task.systemTaskType())) {
            throw error(HttpStatus.BAD_REQUEST, "TASK_NOT_MANUAL_REVIEW", "只有人工复核任务允许转换。");
        }
        TargetTaskType targetTaskType = parseTargetTaskType(request == null ? null : request.targetTaskType());
        ReservationOrderSnapshot originalOrder = workflowRepository.findOrderById(task.hotelId(), task.orderId())
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "ORDER_NOT_FOUND", "原订单不存在。"));
        ReservationOrderSnapshot targetOrder = resolveTargetOrder(task, originalOrder, targetTaskType, request);
        LocalDateTime now = nowUtc();
        updateTaskForManualConversionWithQueueRetry(task, targetOrder, targetTaskType, now);
        boolean originalOrderLogicDeleted = logicDeleteOriginalTemporaryOrderIfEmpty(task, originalOrder, targetOrder, now);
        writeManualConversionAudit(task, originalOrder, targetOrder, targetTaskType, request, originalOrderLogicDeleted, now);
        return new ManualReviewConversionResult(
                task.id().toString(),
                targetOrder.id().toString(),
                originalOrder.id().toString(),
                targetTaskType.systemTaskType().name(),
                targetTaskType.taskCardType().name(),
                task.taskStatus(),
                originalOrderLogicDeleted);
    }

    /**
     * 保存任务卡草稿。草稿允许不完整，但提交字段必须属于当前任务矩阵且符合基础校验。
     */
    @Override
    @Transactional
    public ReservationTaskPayloadMutationResult saveTaskDraft(
            Long taskId,
            ReservationTaskPayloadMutationRequest request) {
        ReservationTaskSnapshot task = findTaskOrThrow(taskId);
        ReservationTaskCardSnapshot taskCard = findTaskCardOrThrow(task);
        ensureTaskEditable(task);

        JsonNode aiPayload = parseJson(taskCard.aiPayloadJson());
        List<ReservationTaskCardFieldDefinition> allDefinitions =
                fieldDefinitionProvider.listDefinitions(task.taskCardType(), task.resultType());
        Map<String, Object> submittedValues = normalizeSubmittedFieldValues(
                allDefinitions,
                request == null ? null : request.fieldValues());
        Map<String, Object> draftValues = normalizeStoredFieldValues(
                allDefinitions,
                fieldValuesFromPayloadJson(taskCard.draftPayloadJson()));
        Map<String, Object> mergedDraftValues = mergeFieldValues(draftValues, submittedValues);
        List<ReservationTaskCardFieldDefinition> activeDefinitions =
                activeDefinitions(task, aiPayload, mergedDraftValues);
        List<String> validationErrors = validateSubmittedFields(activeDefinitions, submittedValues);
        validationErrors.addAll(validateDefinitionValues(task, activeDefinitions, aiPayload, mergedDraftValues,
                submittedValues.keySet(), false));
        if (!validationErrors.isEmpty()) {
            throw validationError(validationErrors);
        }

        LocalDateTime now = nowUtc();
        String draftPayloadJson = toPayloadJson(PAYLOAD_SOURCE_DRAFT, mergedDraftValues, now);
        boolean updated = workflowRepository.updateTaskDraftPayload(task.hotelId(), task.id(), draftPayloadJson, now);
        if (!updated) {
            throw error(HttpStatus.NOT_FOUND, "TASK_CARD_NOT_FOUND", "任务卡不存在。");
        }
        writePayloadMutationAudit(task, ACTION_TASK_DRAFT_SAVE, taskCard.draftPayloadJson(), draftPayloadJson, now);
        return new ReservationTaskPayloadMutationResult(
                task.id().toString(),
                task.orderId().toString(),
                task.taskStatus(),
                jsonPayloadToObject(draftPayloadJson),
                jsonPayloadToObject(taskCard.confirmedPayloadJson()),
                List.of());
    }

    /**
     * 最终确认任务字段。确认会按矩阵做必填、枚举和基础规则校验，并流转为 READY。
     */
    @Override
    @Transactional
    public ReservationTaskPayloadMutationResult confirmTask(
            Long taskId,
            ReservationTaskPayloadMutationRequest request) {
        ReservationTaskSnapshot task = findTaskOrThrow(taskId);
        ReservationTaskCardSnapshot taskCard = findTaskCardOrThrow(task);
        ensureTaskEditable(task);
        ensureGenericConfirmAllowed(task, taskCard);

        JsonNode aiPayload = parseJson(taskCard.aiPayloadJson());
        List<ReservationTaskCardFieldDefinition> allDefinitions =
                fieldDefinitionProvider.listDefinitions(task.taskCardType(), task.resultType());
        Map<String, Object> submittedValues = normalizeSubmittedFieldValues(
                allDefinitions,
                request == null ? null : request.fieldValues());
        Map<String, Object> draftValues = normalizeStoredFieldValues(
                allDefinitions,
                fieldValuesFromPayloadJson(taskCard.draftPayloadJson()));
        Map<String, Object> editedValues = mergeFieldValues(draftValues, submittedValues);
        List<ReservationTaskCardFieldDefinition> activeDefinitions = activeDefinitions(task, aiPayload, editedValues);
        List<String> validationErrors = validateSubmittedFields(activeDefinitions, submittedValues);
        validationErrors.addAll(validateDefinitionValues(task, activeDefinitions, aiPayload, editedValues,
                definitionFieldPaths(activeDefinitions), true, true));
        validationErrors.addAll(validateDateRange(activeDefinitions, aiPayload, editedValues));
        if (!validationErrors.isEmpty()) {
            throw validationError(validationErrors);
        }

        LocalDateTime now = nowUtc();
        Map<String, Object> confirmedValues = buildConfirmedFieldValues(activeDefinitions, aiPayload, editedValues);
        String confirmedPayloadJson = toPayloadJson(PAYLOAD_SOURCE_CONFIRMATION, confirmedValues, now);
        boolean confirmed = workflowRepository.confirmTaskPayload(
                task.hotelId(),
                task.id(),
                confirmedPayloadJson,
                ACTOR_ID_LOCAL,
                now);
        if (!confirmed) {
            throw error(HttpStatus.CONFLICT, "TASK_CONFIRM_CONFLICT", "任务状态已变化，无法确认。");
        }
        writePayloadMutationAudit(task, ACTION_TASK_CONFIRM, taskCard.confirmedPayloadJson(), confirmedPayloadJson, now);
        ReservationTaskSnapshot confirmedTask = findTaskOrThrow(taskId);
        List<ReservationOperaOperationResult> operaOperations =
                createDefaultOperaOperations(confirmedTask, confirmedPayloadJson, now);
        return new ReservationTaskPayloadMutationResult(
                confirmedTask.id().toString(),
                confirmedTask.orderId().toString(),
                confirmedTask.taskStatus(),
                jsonPayloadToObject(taskCard.draftPayloadJson()),
                jsonPayloadToObject(confirmedPayloadJson),
                operaOperations);
    }

    /**
     * 在原业务任务卡上完成 type-known manual_review 复核。字段修正写入确认 payload，不改写 AI 原始快照。
     */
    @Override
    @Transactional
    public ManualReviewResolutionResult resolveManualReview(
            Long taskId,
            ManualReviewResolutionRequest request) {
        ReservationTaskSnapshot task = findTaskOrThrow(taskId);
        ReservationTaskCardSnapshot taskCard = findTaskCardOrThrow(task);
        ensureTypeKnownManualReviewTask(task, taskCard);
        ensureTaskEditable(task);
        validateConfirmedOrder(task, request == null ? null : request.confirmedOrderId());

        JsonNode aiPayload = parseJson(taskCard.aiPayloadJson());
        Map<String, Object> draftValues = fieldValuesFromPayloadJson(taskCard.draftPayloadJson());
        List<ReservationTaskCardFieldDefinition> activeDefinitions = activeDefinitions(task, aiPayload, draftValues);
        List<ManualReviewResolutionFieldOverrideResult> fieldOverrides =
                resolveFieldOverrides(reviewPointerDefinitions(task), request == null ? null : request.fieldOverrides());
        Map<String, Object> submittedValues = fieldOverrideValues(fieldOverrides);
        Map<String, Object> editedValues = mergeFieldValues(draftValues, submittedValues);
        activeDefinitions = activeDefinitions(task, aiPayload, editedValues);
        List<String> validationErrors = validateSubmittedFields(activeDefinitions, submittedValues);
        validationErrors.addAll(validateDefinitionValues(task, activeDefinitions, aiPayload, editedValues,
                submittedValues.keySet(), false));
        validationErrors.addAll(validateDefinitionValues(task, activeDefinitions, aiPayload, editedValues,
                definitionFieldPaths(activeDefinitions), true, true));
        validationErrors.addAll(validateManualReviewMissingFieldsResolved(aiPayload, activeDefinitions, editedValues));
        validationErrors.addAll(validateDateRange(activeDefinitions, aiPayload, editedValues));
        if (!validationErrors.isEmpty()) {
            throw validationError(validationErrors);
        }

        LocalDateTime now = nowUtc();
        Map<String, Object> confirmedValues = buildConfirmedFieldValues(activeDefinitions, aiPayload, editedValues);
        String confirmedPayloadJson = toPayloadJson(PAYLOAD_SOURCE_MANUAL_REVIEW_RESOLUTION, confirmedValues, now);
        String reviewResolutionJson = toReviewResolutionJson(task, request, fieldOverrides, now);
        boolean resolved = workflowRepository.resolveManualReview(
                task.hotelId(),
                task.id(),
                ReservationReviewStatus.RESOLVED.name(),
                reviewResolutionJson,
                confirmedPayloadJson,
                ACTOR_ID_LOCAL,
                now);
        if (!resolved) {
            throw error(HttpStatus.CONFLICT, "TASK_REVIEW_RESOLVE_CONFLICT", "任务状态已变化，无法完成复核。");
        }
        writeManualReviewResolveAudit(task, taskCard, reviewResolutionJson, request, now);
        ReservationTaskSnapshot resolvedTask = findTaskOrThrow(taskId);
        List<ReservationOperaOperationResult> operaOperations =
                createDefaultOperaOperations(resolvedTask, confirmedPayloadJson, now);
        return new ManualReviewResolutionResult(
                resolvedTask.id().toString(),
                resolvedTask.orderId().toString(),
                resolvedTask.taskStatus(),
                ReservationReviewStatus.RESOLVED.name(),
                jsonPayloadToObject(reviewResolutionJson),
                jsonPayloadToObject(confirmedPayloadJson),
                operaOperations);
    }

    /**
     * 执行 OPERA 模拟操作。第一版不接真实 OPERA，只根据请求开关生成模拟结果和 attempt。
     */
    @Override
    @Transactional
    public ReservationOperaOperationResult executeOperaOperation(
            Long taskId,
            Long operationId,
            ReservationOperaSimulationRequest request) {
        return executeOperaOperationInternal(taskId, operationId, request, false);
    }

    /**
     * 重试失败的 OPERA 模拟操作。重试会新增 attempt，不覆盖历史结果。
     */
    @Override
    @Transactional
    public ReservationOperaOperationResult retryOperaOperation(
            Long taskId,
            Long operationId,
            ReservationOperaSimulationRequest request) {
        return executeOperaOperationInternal(taskId, operationId, request, true);
    }

    /**
     * 查询任务审计流水。第一版只按任务维度返回后端审计列表。
     */
    @Override
    @Transactional(readOnly = true)
    public ReservationTaskAuditListResult listTaskAudits(Long taskId) {
        ReservationTaskSnapshot task = findTaskOrThrow(taskId);
        List<ReservationTaskAuditLogResult> items = workflowRepository
                .findAuditLogsByTaskId(task.hotelId(), task.id())
                .stream()
                .map(this::toAuditLogResult)
                .toList();
        return new ReservationTaskAuditListResult(task.id().toString(), items);
    }

    /**
     * 为已确认任务生成第一版固定两条 OPERA 模拟操作。
     */
    private List<ReservationOperaOperationResult> createDefaultOperaOperations(
            ReservationTaskSnapshot task,
            String confirmedPayloadJson,
            LocalDateTime now) {
        List<ReservationOperaOperationSnapshot> existingOperations =
                workflowRepository.findOperaOperationsByTaskId(task.hotelId(), task.id());
        if (!existingOperations.isEmpty()) {
            return existingOperations.stream()
                    .map(operation -> toOperaOperationResult(operation, List.of()))
                    .toList();
        }
        List<ReservationOperaOperationDraft> drafts = List.of(
                new ReservationOperaOperationDraft(
                        task.hotelId(),
                        task.orderId(),
                        task.id(),
                        1,
                        OPERA_OPERATION_CODE_PRECHECK,
                        "OPERA 模拟预检查",
                        ReservationOperaOperationStatus.PENDING.name(),
                        operaOperationRequestPayload(task, confirmedPayloadJson, OPERA_OPERATION_CODE_PRECHECK),
                        now),
                new ReservationOperaOperationDraft(
                        task.hotelId(),
                        task.orderId(),
                        task.id(),
                        2,
                        OPERA_OPERATION_CODE_WRITE,
                        "OPERA 模拟写入",
                        ReservationOperaOperationStatus.PENDING.name(),
                        operaOperationRequestPayload(task, confirmedPayloadJson, OPERA_OPERATION_CODE_WRITE),
                        now));
        for (ReservationOperaOperationDraft draft : drafts) {
            workflowRepository.insertOperaOperation(draft);
        }
        return workflowRepository.findOperaOperationsByTaskId(task.hotelId(), task.id())
                .stream()
                .map(operation -> toOperaOperationResult(operation, List.of()))
                .toList();
    }

    /**
     * 执行或重试 OPERA 模拟操作，并记录 attempt 和审计。
     */
    private ReservationOperaOperationResult executeOperaOperationInternal(
            Long taskId,
            Long operationId,
            ReservationOperaSimulationRequest request,
            boolean retry) {
        ReservationTaskSnapshot task = findTaskOrThrow(taskId);
        ReservationOperaOperationSnapshot operation = workflowRepository
                .findOperaOperationById(task.hotelId(), operationId)
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "OPERA_OPERATION_NOT_FOUND", "OPERA 模拟操作不存在。"));
        if (!task.id().equals(operation.taskId())) {
            throw error(HttpStatus.NOT_FOUND, "OPERA_OPERATION_NOT_FOUND", "OPERA 模拟操作不存在。");
        }
        validateOperaOperationExecutable(task, operation, retry);

        boolean simulateSuccess = request == null || !Boolean.FALSE.equals(request.simulateSuccess());
        String attemptStatus = simulateSuccess
                ? ReservationOperaAttemptStatus.SUCCEEDED.name()
                : ReservationOperaAttemptStatus.FAILED.name();
        String operationStatus = simulateSuccess
                ? ReservationOperaOperationStatus.SUCCEEDED.name()
                : ReservationOperaOperationStatus.FAILED.name();
        String errorMessage = simulateSuccess
                ? null
                : defaultFailureMessage(request == null ? null : request.failureMessage());
        LocalDateTime now = nowUtc();
        Integer attemptNumber = (operation.attemptCount() == null ? 0 : operation.attemptCount()) + 1;
        String responsePayloadJson = operaAttemptResponsePayload(operation, attemptNumber, simulateSuccess, errorMessage, now);
        Long attemptId = workflowRepository.insertOperaOperationAttempt(new ReservationOperaOperationAttemptDraft(
                task.hotelId(),
                task.orderId(),
                task.id(),
                operation.id(),
                attemptNumber,
                attemptStatus,
                operation.requestPayloadJson(),
                responsePayloadJson,
                errorMessage,
                now,
                now));
        workflowRepository.updateOperaOperationAfterAttempt(
                task.hotelId(),
                operation.id(),
                operationStatus,
                attemptId,
                errorMessage,
                now);
        writeOperaOperationAudit(task, operation, retry, operationStatus, attemptNumber, errorMessage, now);
        completeTaskWhenAllOperaOperationsSucceeded(task, operation.id(), operationStatus, now);

        ReservationOperaOperationSnapshot updatedOperation = workflowRepository
                .findOperaOperationById(task.hotelId(), operation.id())
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "OPERA_OPERATION_NOT_FOUND", "OPERA 模拟操作不存在。"));
        List<ReservationOperaOperationAttemptSnapshot> attempts =
                workflowRepository.findOperaOperationAttempts(task.hotelId(), operation.id());
        return toOperaOperationResult(updatedOperation, attempts);
    }

    /**
     * 校验 OPERA 模拟操作执行顺序和重试状态。
     */
    private void validateOperaOperationExecutable(
            ReservationTaskSnapshot task,
            ReservationOperaOperationSnapshot operation,
            boolean retry) {
        if (!ReservationTaskStatus.READY.name().equals(task.taskStatus())) {
            throw error(HttpStatus.CONFLICT, "TASK_STATUS_NOT_EXECUTABLE", "只有 READY 任务允许执行 OPERA 模拟操作。");
        }
        ReservationTaskAvailabilityResult availability = availabilityResolver.calculateAvailability(task);
        if (availability.readOnly() || !availability.executable()) {
            throw error(HttpStatus.CONFLICT, "TASK_READ_ONLY", "任务当前只读，不能执行 OPERA 模拟操作。");
        }
        if (retry) {
            if (!ReservationOperaOperationStatus.FAILED.name().equals(operation.operationStatus())) {
                throw error(HttpStatus.CONFLICT, "OPERA_OPERATION_NOT_RETRYABLE", "只有失败的 OPERA 模拟操作允许重试。");
            }
        } else if (!ReservationOperaOperationStatus.PENDING.name().equals(operation.operationStatus())) {
            throw error(HttpStatus.CONFLICT, "OPERA_OPERATION_NOT_EXECUTABLE", "只有待执行的 OPERA 模拟操作允许执行。");
        }
        boolean previousSucceeded = workflowRepository.findOperaOperationsByTaskId(task.hotelId(), task.id())
                .stream()
                .filter(item -> item.operationSequence() < operation.operationSequence())
                .allMatch(item -> ReservationOperaOperationStatus.SUCCEEDED.name().equals(item.operationStatus()));
        if (!previousSucceeded) {
            throw error(
                    HttpStatus.CONFLICT,
                    "OPERA_PREVIOUS_OPERATION_NOT_SUCCEEDED",
                    "前置 OPERA 模拟操作未成功，不能执行当前操作。");
        }
    }

    /**
     * 所有 OPERA 模拟操作成功后，将任务标记为 COMPLETED。
     */
    private void completeTaskWhenAllOperaOperationsSucceeded(
            ReservationTaskSnapshot task,
            Long updatedOperationId,
            String updatedOperationStatus,
            LocalDateTime now) {
        boolean allSucceeded = workflowRepository.findOperaOperationsByTaskId(task.hotelId(), task.id())
                .stream()
                .map(operation -> operation.id().equals(updatedOperationId)
                        ? ReservationOperaOperationStatus.SUCCEEDED.name().equals(updatedOperationStatus)
                        : ReservationOperaOperationStatus.SUCCEEDED.name().equals(operation.operationStatus()))
                .allMatch(Boolean::booleanValue);
        if (allSucceeded) {
            workflowRepository.updateTaskStatus(
                    task.hotelId(),
                    task.id(),
                    ReservationTaskStatus.COMPLETED.name(),
                    now);
        }
    }

    /**
     * 构建 OPERA 模拟操作请求 JSON。第一版只保存 confirmed payload 的摘要和操作代码。
     */
    private String operaOperationRequestPayload(
            ReservationTaskSnapshot task,
            String confirmedPayloadJson,
            String operationCode) {
        Map<String, Object> fieldValues = fieldValuesFromPayloadJson(confirmedPayloadJson);
        return toJson(Map.of(
                "schema_version", "opera-simulation-v1",
                "task_id", task.id().toString(),
                "operation_code", operationCode,
                "confirmed_payload_schema_version", PAYLOAD_SCHEMA_VERSION,
                "confirmed_field_count", fieldValues.size()));
    }

    /**
     * 构建 OPERA 模拟 attempt 响应 JSON。
     */
    private String operaAttemptResponsePayload(
            ReservationOperaOperationSnapshot operation,
            Integer attemptNumber,
            boolean simulateSuccess,
            String errorMessage,
            LocalDateTime now) {
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("schema_version", "opera-simulation-response-v1");
        response.put("operation_id", operation.id().toString());
        response.put("operation_code", operation.operationCode());
        response.put("attempt_number", attemptNumber);
        response.put("success", simulateSuccess);
        response.put("simulated_at", now.toString());
        response.put("error_message", errorMessage);
        return toJson(response);
    }

    /**
     * 写入 OPERA 模拟操作执行或重试审计。
     */
    private void writeOperaOperationAudit(
            ReservationTaskSnapshot task,
            ReservationOperaOperationSnapshot operation,
            boolean retry,
            String operationStatus,
            Integer attemptNumber,
            String errorMessage,
            LocalDateTime now) {
        Map<String, Object> afterSnapshot = new LinkedHashMap<>();
        afterSnapshot.put("operation_status", operationStatus);
        afterSnapshot.put("attempt_number", attemptNumber);
        afterSnapshot.put("error_message", errorMessage);
        workflowRepository.insertAuditLog(new ReservationAuditLogDraft(
                task.hotelId(),
                task.orderId(),
                task.id(),
                operation.id(),
                ACTOR_TYPE_USER,
                ACTOR_ID_LOCAL,
                retry ? ACTION_OPERA_OPERATION_RETRY : ACTION_OPERA_OPERATION_EXECUTE,
                errorMessage,
                toJson(Map.of(
                        "operation_status", operation.operationStatus(),
                        "attempt_count", operation.attemptCount())),
                toJson(afterSnapshot),
                now));
    }

    /**
     * 转换 OPERA 模拟操作快照为响应。
     */
    private ReservationOperaOperationResult toOperaOperationResult(
            ReservationOperaOperationSnapshot operation,
            List<ReservationOperaOperationAttemptSnapshot> attempts) {
        return new ReservationOperaOperationResult(
                operation.id().toString(),
                operation.taskId().toString(),
                operation.operationSequence(),
                operation.operationCode(),
                operation.operationName(),
                operation.operationStatus(),
                operation.attemptCount(),
                operation.lastErrorMessage(),
                attempts.stream().map(this::toOperaOperationAttemptResult).toList());
    }

    /**
     * 查询任务下 OPERA 模拟操作及 attempt 历史，用于任务详情页一次性展示执行轨迹。
     */
    private List<ReservationOperaOperationResult> findOperaOperationResults(ReservationTaskSnapshot task) {
        return workflowRepository.findOperaOperationsByTaskId(task.hotelId(), task.id())
                .stream()
                .map(operation -> toOperaOperationResult(
                        operation,
                        workflowRepository.findOperaOperationAttempts(task.hotelId(), operation.id())))
                .toList();
    }

    /**
     * 转换 OPERA 模拟操作 attempt 快照为响应。
     */
    private ReservationOperaOperationAttemptResult toOperaOperationAttemptResult(
            ReservationOperaOperationAttemptSnapshot attempt) {
        return new ReservationOperaOperationAttemptResult(
                attempt.id().toString(),
                attempt.attemptNumber(),
                attempt.attemptStatus(),
                jsonPayloadToObject(attempt.responsePayloadJson()),
                attempt.errorMessage(),
                attempt.startedAt().toString(),
                attempt.finishedAt().toString());
    }

    /**
     * 转换任务审计快照为响应。
     */
    private ReservationTaskAuditLogResult toAuditLogResult(ReservationAuditLogSnapshot snapshot) {
        return new ReservationTaskAuditLogResult(
                snapshot.id().toString(),
                snapshot.orderId() == null ? null : snapshot.orderId().toString(),
                snapshot.taskId() == null ? null : snapshot.taskId().toString(),
                snapshot.operationId() == null ? null : snapshot.operationId().toString(),
                snapshot.actorType(),
                snapshot.actorId(),
                snapshot.action(),
                snapshot.reason(),
                jsonPayloadToObject(snapshot.beforeSnapshotJson()),
                jsonPayloadToObject(snapshot.afterSnapshotJson()),
                snapshot.occurredAt().toString());
    }

    /**
     * 默认模拟失败原因，避免空错误影响前端展示。
     */
    private String defaultFailureMessage(String rawFailureMessage) {
        String failureMessage = trimToNull(rawFailureMessage);
        return failureMessage == null ? "OPERA 模拟失败。" : failureMessage;
    }

    /**
     * 更新人工复核任务；迁移到其他订单时，如果并发撞到队列序号则重新取号重试。
     */
    private void updateTaskForManualConversionWithQueueRetry(
            ReservationTaskSnapshot task,
            ReservationOrderSnapshot targetOrder,
            TargetTaskType targetTaskType,
            LocalDateTime now) {
        for (int attempt = 1; attempt <= MAX_QUEUE_ORDER_RETRY; attempt++) {
            int targetExecutionOrder = targetOrder.id().equals(task.orderId())
                    ? task.executionOrder()
                    : workflowRepository.nextExecutionOrder(task.hotelId(), targetOrder.id());
            try {
                workflowRepository.updateTaskForManualConversion(
                        task.hotelId(),
                        task.id(),
                        targetOrder.id(),
                        targetTaskType.aiTaskType(),
                        targetTaskType.systemTaskType().name(),
                        targetTaskType.taskCardType().name(),
                        targetExecutionOrder,
                        now);
                return;
            } catch (DuplicateKeyException exception) {
                if (targetOrder.id().equals(task.orderId()) || attempt == MAX_QUEUE_ORDER_RETRY) {
                    throw error(HttpStatus.CONFLICT, "TASK_QUEUE_ORDER_CONFLICT", "同订单任务队列序号并发写入冲突。");
                }
            }
        }
        throw error(HttpStatus.CONFLICT, "TASK_QUEUE_ORDER_CONFLICT", "同订单任务队列序号并发写入冲突。");
    }

    /**
     * 查询任务，不存在时抛出受控 404。
     */
    private ReservationTaskSnapshot findTaskOrThrow(Long taskId) {
        if (taskId == null) {
            throw error(HttpStatus.NOT_FOUND, "TASK_NOT_FOUND", "任务不存在。");
        }
        return workflowRepository.findTaskById(taskId)
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "TASK_NOT_FOUND", "任务不存在。"));
    }

    /**
     * 查询任务卡，不存在时抛出受控 404。
     */
    private ReservationTaskCardSnapshot findTaskCardOrThrow(ReservationTaskSnapshot task) {
        return workflowRepository.findTaskCardByTaskId(task.hotelId(), task.id())
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "TASK_CARD_NOT_FOUND", "任务卡不存在。"));
    }

    /**
     * 校验任务当前是否允许人工编辑或最终确认。
     */
    private void ensureTaskEditable(ReservationTaskSnapshot task) {
        if (!ReservationTaskStatus.PENDING_CONFIRM.name().equals(task.taskStatus())) {
            throw error(HttpStatus.CONFLICT, "TASK_STATUS_NOT_EDITABLE", "只有 PENDING_CONFIRM 任务允许编辑或确认。");
        }
        ReservationTaskAvailabilityResult availability = availabilityResolver.calculateAvailability(task);
        if (availability.readOnly() || !availability.editable() || !availability.confirmable()) {
            throw error(HttpStatus.CONFLICT, "TASK_READ_ONLY", "任务当前只读，不能编辑或确认。");
        }
    }

    /**
     * 校验当前任务是已知业务类型的 manual_review，同卡解阻不处理 Fallback 人工转换任务。
     */
    private void ensureTypeKnownManualReviewTask(
            ReservationTaskSnapshot task,
            ReservationTaskCardSnapshot taskCard) {
        if (!isTypeKnownManualReviewTask(task) && trimToNull(taskCard.reviewStatus()) == null) {
            throw error(HttpStatus.BAD_REQUEST, "TASK_NOT_TYPE_KNOWN_MANUAL_REVIEW", "当前任务不是同卡人工复核任务。");
        }
    }

    /**
     * 通用确认接口不能处理 type-known manual_review，避免绕过复核解阻审计和 field_overrides 保存。
     */
    private void ensureGenericConfirmAllowed(
            ReservationTaskSnapshot task,
            ReservationTaskCardSnapshot taskCard) {
        if (isTypeKnownManualReviewTask(task) || trimToNull(taskCard.reviewStatus()) != null) {
            throw error(
                    HttpStatus.BAD_REQUEST,
                    "TASK_REVIEW_RESOLUTION_REQUIRED",
                    "type-known manual_review 任务必须通过复核解阻接口确认。");
        }
    }

    /**
     * 判断任务是否为已知业务类型的 manual_review，而非 Fallback 人工转换任务。
     */
    private boolean isTypeKnownManualReviewTask(ReservationTaskSnapshot task) {
        return AiResultType.MANUAL_REVIEW.code().equals(task.resultType())
                && !ReservationSystemTaskType.MANUAL_REVIEW.name().equals(task.systemTaskType());
    }

    /**
     * 校验复核确认的订单归属。第一版只允许确认当前订单，不提供普通任务任意切换订单能力。
     */
    private void validateConfirmedOrder(ReservationTaskSnapshot task, String rawConfirmedOrderId) {
        String confirmedOrderIdText = trimToNull(rawConfirmedOrderId);
        if (confirmedOrderIdText == null) {
            throw error(HttpStatus.BAD_REQUEST, "REVIEW_ORDER_REQUIRED", "confirmed_order_id 必填。");
        }
        Long confirmedOrderId = parseLong(confirmedOrderIdText, "REVIEW_ORDER_INVALID", "confirmed_order_id 格式无效。");
        if (!task.orderId().equals(confirmedOrderId)) {
            throw new ReservationTaskWorkflowException(
                    HttpStatus.BAD_REQUEST,
                    "REVIEW_ORDER_ASSIGNMENT_MISMATCH",
                    "复核确认订单与当前任务订单不一致。",
                    List.of("confirmed_order_id: 第一版必须等于当前任务所属订单。"));
        }
    }

    /**
     * 将复核字段 JSON Pointer 校验并映射到矩阵 field_path，只允许当前任务卡可编辑字段。
     */
    private List<ManualReviewResolutionFieldOverrideResult> resolveFieldOverrides(
            List<ReservationTaskCardFieldDefinition> activeDefinitions,
            List<ManualReviewResolutionFieldOverrideRequest> requests) {
        if (requests == null || requests.isEmpty()) {
            return List.of();
        }
        Map<String, ReservationTaskCardFieldDefinition> definitionByPath = new LinkedHashMap<>();
        activeDefinitions.forEach(definition -> definitionByPath.put(definition.fieldPath(), definition));
        List<ManualReviewResolutionFieldOverrideResult> results = new ArrayList<>();
        Set<String> seenPointers = new LinkedHashSet<>();
        Set<String> seenFieldPaths = new LinkedHashSet<>();
        for (ManualReviewResolutionFieldOverrideRequest request : requests) {
            String fieldPointer = trimToNull(request == null ? null : request.fieldPointer());
            String fieldPath = trimToNull(request == null ? null : request.fieldPath());
            ReservationTaskCardFieldDefinition definition =
                    findEditableDefinitionByOverride(definitionByPath, fieldPointer, fieldPath);
            String resultFieldPointer = fieldPointer != null ? fieldPointer : fieldPointerFor(definition.fieldPath());
            if (!seenPointers.add(resultFieldPointer) || !seenFieldPaths.add(definition.fieldPath())) {
                throw reviewPointerDuplicateError(resultFieldPointer, definition.fieldPath());
            }
            results.add(new ManualReviewResolutionFieldOverrideResult(
                    resultFieldPointer,
                    definition.fieldPath(),
                    legacyFieldPathFor(definition.fieldPath()),
                    request.value()));
        }
        return results;
    }

    /**
     * 构建重复复核字段错误，避免 confirmed payload 后值覆盖前值但审计保留多条冲突记录。
     */
    private ReservationTaskWorkflowException reviewPointerDuplicateError(String fieldPointer, String fieldPath) {
        return new ReservationTaskWorkflowException(
                HttpStatus.BAD_REQUEST,
                "TASK_REVIEW_POINTER_DUPLICATE",
                "复核字段指针重复。",
                List.of(fieldPointer + ": 重复指向字段 " + fieldPath + "。"));
    }

    /**
     * 复核 pointer 白名单使用当前卡型的可编辑字段全集；展示条件由合并后的 activeDefinitions 再兜底校验。
     */
    private List<ReservationTaskCardFieldDefinition> reviewPointerDefinitions(ReservationTaskSnapshot task) {
        return fieldDefinitionProvider.listDefinitions(task.taskCardType(), task.resultType())
                .stream()
                .filter(definition -> isYes(definition.visible()))
                .filter(definition -> matchesTaskResultType(definition.resultType(), task))
                .filter(definition -> matchesMatrixExpression(definition.taskType(), task.aiTaskType()))
                .filter(definition -> matchesMatrixExpression(definition.taskSubtype(), task.taskSubtype()))
                .toList();
    }

    /**
     * 按 JSON Pointer 查找可编辑字段定义。
     */
    private ReservationTaskCardFieldDefinition findEditableDefinitionByPointer(
            Map<String, ReservationTaskCardFieldDefinition> definitionByPath,
            String fieldPointer) {
        if (fieldPointer == null || !fieldPointer.startsWith("/")) {
            throw reviewPointerError(fieldPointer);
        }
        for (String fieldPath : fieldPathCandidatesFromPointer(fieldPointer)) {
            ReservationTaskCardFieldDefinition definition = definitionByPath.get(fieldPath);
            if (definition != null && isYes(definition.editable())) {
                return definition;
            }
            if (definition != null) {
                throw reviewPointerError(fieldPointer);
            }
        }
        throw reviewPointerError(fieldPointer);
    }

    /**
     * 按复核请求定位可编辑字段，支持 JSON Pointer、P0 主 field_path 和旧扁平 field_path。
     */
    private ReservationTaskCardFieldDefinition findEditableDefinitionByOverride(
            Map<String, ReservationTaskCardFieldDefinition> definitionByPath,
            String fieldPointer,
            String fieldPath) {
        ReservationTaskCardFieldDefinition pointerDefinition = null;
        if (fieldPointer != null) {
            pointerDefinition = findEditableDefinitionByPointer(definitionByPath, fieldPointer);
        }
        ReservationTaskCardFieldDefinition pathDefinition = null;
        if (fieldPath != null) {
            pathDefinition = findEditableDefinitionByFieldPath(definitionByPath, fieldPath);
        }
        if (pointerDefinition != null && pathDefinition != null
                && !pointerDefinition.fieldPath().equals(pathDefinition.fieldPath())) {
            throw reviewPointerError(fieldPointer + " / " + fieldPath);
        }
        if (pointerDefinition != null) {
            return pointerDefinition;
        }
        if (pathDefinition != null) {
            return pathDefinition;
        }
        throw reviewPointerError(null);
    }

    /**
     * 按矩阵字段路径查找可编辑字段，先将旧扁平路径归一化到 P0 主路径。
     */
    private ReservationTaskCardFieldDefinition findEditableDefinitionByFieldPath(
            Map<String, ReservationTaskCardFieldDefinition> definitionByPath,
            String fieldPath) {
        String canonicalFieldPath = canonicalSubmittedFieldPath(definitionByPath, fieldPath);
        ReservationTaskCardFieldDefinition definition = definitionByPath.get(canonicalFieldPath);
        if (definition == null || !isYes(definition.editable())) {
            throw reviewPointerError(fieldPath);
        }
        return definition;
    }

    /**
     * 将 RFC 6901 JSON Pointer 转换为可能的矩阵 field_path，兼容数组下标到 [] 与旧扁平路径。
     */
    private List<String> fieldPathCandidatesFromPointer(String fieldPointer) {
        String[] rawTokens = fieldPointer.substring(1).split("/", -1);
        List<String> plainTokens = new ArrayList<>();
        List<String> collapsedArrayTokens = new ArrayList<>();
        for (String rawToken : rawTokens) {
            String token = unescapeJsonPointerToken(rawToken, fieldPointer);
            if (token.isEmpty()) {
                throw reviewPointerError(fieldPointer);
            }
            plainTokens.add(token);
            if (isNumericToken(token)) {
                if (collapsedArrayTokens.isEmpty()) {
                    throw reviewPointerError(fieldPointer);
                }
                int lastIndex = collapsedArrayTokens.size() - 1;
                String previous = collapsedArrayTokens.get(lastIndex);
                collapsedArrayTokens.set(lastIndex, previous.endsWith("[]") ? previous : previous + "[]");
            } else {
                collapsedArrayTokens.add(token);
            }
        }
        List<String> candidates = new ArrayList<>();
        candidates.add(String.join(".", plainTokens));
        String collapsed = String.join(".", collapsedArrayTokens);
        if (!collapsed.equals(candidates.get(0))) {
            candidates.add(collapsed);
        }
        for (String legacyAlias : p0PointerFieldPathAliases(candidates.get(0), collapsed)) {
            if (!candidates.contains(legacyAlias)) {
                candidates.add(legacyAlias);
            }
        }
        return candidates;
    }

    /**
     * 兼容 P0 room_items[] 和旧扁平字段在复核指针中的双向过渡。
     */
    private List<String> p0PointerFieldPathAliases(String plainFieldPath, String collapsedFieldPath) {
        String legacyAlias = legacyFieldPathFor(plainFieldPath);
        if (legacyAlias != null) {
            return List.of(legacyAlias);
        }
        String canonicalAlias = canonicalP0RoomItemFieldPath(plainFieldPath);
        if (canonicalAlias != null) {
            return List.of(canonicalAlias);
        }
        return List.of();
    }

    /**
     * 解析 JSON Pointer token，拒绝非法的 ~ 转义。
     */
    private String unescapeJsonPointerToken(String rawToken, String fieldPointer) {
        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()) {
                throw reviewPointerError(fieldPointer);
            }
            char escaped = rawToken.charAt(++i);
            if (escaped == '0') {
                builder.append('~');
            } else if (escaped == '1') {
                builder.append('/');
            } else {
                throw reviewPointerError(fieldPointer);
            }
        }
        return builder.toString();
    }

    /**
     * 判断 JSON Pointer token 是否为数组下标。
     */
    private boolean isNumericToken(String token) {
        if (token == null || token.isBlank()) {
            return false;
        }
        for (int i = 0; i < token.length(); i++) {
            if (!Character.isDigit(token.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * 将复核字段修正结果转换为 field_path -> value，用于复用现有字段矩阵校验。
     */
    private Map<String, Object> fieldOverrideValues(
            List<ManualReviewResolutionFieldOverrideResult> fieldOverrides) {
        Map<String, Object> values = new LinkedHashMap<>();
        for (ManualReviewResolutionFieldOverrideResult fieldOverride : fieldOverrides) {
            values.put(fieldOverride.fieldPath(), fieldOverride.value());
        }
        return values;
    }

    /**
     * 校验 SuperAgent 明确标记的缺失字段已在复核解阻后有值，避免只靠矩阵必填遗漏业务阻塞点。
     */
    private List<String> validateManualReviewMissingFieldsResolved(
            JsonNode aiPayload,
            List<ReservationTaskCardFieldDefinition> activeDefinitions,
            Map<String, Object> editedValues) {
        JsonNode missingFieldsNode = aiPayload.path("manual_review").path("missing_fields");
        if (!missingFieldsNode.isArray() || missingFieldsNode.isEmpty()) {
            return List.of();
        }
        Map<String, ReservationTaskCardFieldDefinition> definitionByPath = new LinkedHashMap<>();
        activeDefinitions.forEach(definition -> definitionByPath.put(definition.fieldPath(), definition));
        List<String> errors = new ArrayList<>();
        for (JsonNode missingFieldNode : missingFieldsNode) {
            String fieldPointer = trimToNull(missingFieldNode.asText(null));
            ReservationTaskCardFieldDefinition definition = findEditableDefinitionByPointer(definitionByPath, fieldPointer);
            if (isEmptyValue(editedValues.get(definition.fieldPath()))) {
                errors.add(fieldPointer + ": 复核缺失字段仍未补齐。");
            }
        }
        return errors;
    }

    /**
     * 构建同卡人工复核解阻结果 JSON，保存人工修正和订单归属确认，不写回 AI 原始 payload。
     */
    private String toReviewResolutionJson(
            ReservationTaskSnapshot task,
            ManualReviewResolutionRequest request,
            List<ManualReviewResolutionFieldOverrideResult> fieldOverrides,
            LocalDateTime now) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("schema_version", "manual-review-resolution-v1");
        payload.put("resolved_at", UtcTimeFormatter.toUtcOffsetDateTime(now).toString());
        payload.put("resolved_by", ACTOR_ID_LOCAL);
        payload.put("confirmed_order_id", task.orderId().toString());
        payload.put("reason", request == null ? null : trimToNull(request.reason()));
        payload.put("field_overrides", fieldOverrides);
        return toJson(payload);
    }

    /**
     * 写入同卡人工复核解阻审计。审计只保存复核摘要，完整字段值保存在任务卡确认 payload。
     */
    private void writeManualReviewResolveAudit(
            ReservationTaskSnapshot task,
            ReservationTaskCardSnapshot taskCard,
            String reviewResolutionJson,
            ManualReviewResolutionRequest request,
            LocalDateTime now) {
        Map<String, Object> beforeSnapshot = new LinkedHashMap<>();
        beforeSnapshot.put("review_status", taskCard.reviewStatus());
        beforeSnapshot.put("has_confirmed_payload", trimToNull(taskCard.confirmedPayloadJson()) != null);
        Map<String, Object> afterSnapshot = new LinkedHashMap<>();
        afterSnapshot.put("review_status", ReservationReviewStatus.RESOLVED.name());
        afterSnapshot.put("resolution", jsonPayloadToObject(reviewResolutionJson));
        workflowRepository.insertAuditLog(new ReservationAuditLogDraft(
                task.hotelId(),
                task.orderId(),
                task.id(),
                null,
                ACTOR_TYPE_USER,
                ACTOR_ID_LOCAL,
                ACTION_MANUAL_REVIEW_RESOLVE,
                request == null ? null : trimToNull(request.reason()),
                toJson(beforeSnapshot),
                toJson(afterSnapshot),
                now));
    }

    /**
     * 构建 JSON Pointer 错误，details 保留原始 pointer 方便前端定位。
     */
    private ReservationTaskWorkflowException reviewPointerError(String fieldPointer) {
        String pointer = fieldPointer == null ? "<null>" : fieldPointer;
        return new ReservationTaskWorkflowException(
                HttpStatus.BAD_REQUEST,
                "TASK_REVIEW_POINTER_INVALID",
                "复核字段指针无效。",
                List.of(pointer + ": 必须指向当前任务卡允许编辑字段。"));
    }

    /**
     * 标准化请求中的 field_values。支持前端传矩阵 field_path、RFC 6901 pointer 或旧扁平字段。
     */
    private Map<String, Object> normalizeSubmittedFieldValues(
            List<ReservationTaskCardFieldDefinition> definitions,
            Map<String, Object> rawFieldValues) {
        Map<String, Object> normalized = new LinkedHashMap<>();
        if (rawFieldValues == null) {
            return normalized;
        }
        Map<String, ReservationTaskCardFieldDefinition> definitionByPath = definitionByPath(definitions);
        List<String> errors = new ArrayList<>();
        rawFieldValues.forEach((rawFieldPath, value) -> {
            String fieldPath = rawFieldPath == null ? "" : rawFieldPath.trim();
            String canonicalFieldPath = canonicalSubmittedFieldPath(definitionByPath, fieldPath);
            if (normalized.containsKey(canonicalFieldPath)) {
                errors.add(fieldPath + ": 与其他提交字段指向同一字段 " + canonicalFieldPath + "。");
                return;
            }
            normalized.put(canonicalFieldPath, value);
        });
        if (!errors.isEmpty()) {
            throw validationError(errors);
        }
        return normalized;
    }

    /**
     * 标准化已保存草稿里的历史字段 key，避免旧 payload 影响 P0 新矩阵校验。
     */
    private Map<String, Object> normalizeStoredFieldValues(
            List<ReservationTaskCardFieldDefinition> definitions,
            Map<String, Object> storedFieldValues) {
        Map<String, ReservationTaskCardFieldDefinition> definitionByPath = definitionByPath(definitions);
        Map<String, Object> normalized = new LinkedHashMap<>();
        storedFieldValues.forEach((fieldPath, value) -> {
            String canonicalFieldPath = canonicalSubmittedFieldPath(definitionByPath, fieldPath);
            normalized.put(canonicalFieldPath, value);
        });
        return normalized;
    }

    /**
     * 构建字段定义索引，供提交字段和复核 pointer 映射使用。
     */
    private Map<String, ReservationTaskCardFieldDefinition> definitionByPath(
            List<ReservationTaskCardFieldDefinition> definitions) {
        Map<String, ReservationTaskCardFieldDefinition> definitionByPath = new LinkedHashMap<>();
        definitions.forEach(definition -> definitionByPath.put(definition.fieldPath(), definition));
        return definitionByPath;
    }

    /**
     * 将提交字段 key 转成当前矩阵主 field_path；未知字段保留原值，后续由矩阵校验报错。
     */
    private String canonicalSubmittedFieldPath(
            Map<String, ReservationTaskCardFieldDefinition> definitionByPath,
            String fieldPathOrPointer) {
        String normalized = trimToNull(fieldPathOrPointer);
        if (normalized == null) {
            return "";
        }
        if (normalized.startsWith("/")) {
            for (String candidate : fieldPathCandidatesFromPointer(normalized)) {
                if (definitionByPath.containsKey(candidate)) {
                    return candidate;
                }
            }
            return normalized;
        }
        if (definitionByPath.containsKey(normalized)) {
            return normalized;
        }
        String canonicalAlias = canonicalP0RoomItemFieldPath(normalized);
        if (canonicalAlias != null && definitionByPath.containsKey(canonicalAlias)) {
            return canonicalAlias;
        }
        return normalized;
    }

    /**
     * 合并已有草稿和本次提交字段。本次提交优先，用于草稿增量保存和最终确认。
     */
    private Map<String, Object> mergeFieldValues(
            Map<String, Object> existingValues,
            Map<String, Object> submittedValues) {
        Map<String, Object> merged = new LinkedHashMap<>(existingValues);
        merged.putAll(submittedValues);
        return merged;
    }

    /**
     * 根据当前任务类型、subtype 和展示条件筛出本次需要参与后端校验的字段。
     */
    private List<ReservationTaskCardFieldDefinition> activeDefinitions(
            ReservationTaskSnapshot task,
            JsonNode aiPayload,
            Map<String, Object> editedValues) {
        return fieldDefinitionProvider.listDefinitions(task.taskCardType(), task.resultType())
                .stream()
                .filter(definition -> isYes(definition.visible()))
                .filter(definition -> matchesTaskResultType(definition.resultType(), task))
                .filter(definition -> matchesMatrixExpression(definition.taskType(), task.aiTaskType()))
                .filter(definition -> matchesMatrixExpression(definition.taskSubtype(), task.taskSubtype()))
                .filter(definition -> displayConditionMatches(task, definition, aiPayload, editedValues))
                .toList();
    }

    /**
     * 校验本次提交的字段是否属于当前任务卡且允许编辑。
     */
    private List<String> validateSubmittedFields(
            List<ReservationTaskCardFieldDefinition> activeDefinitions,
            Map<String, Object> submittedValues) {
        Map<String, ReservationTaskCardFieldDefinition> definitionByPath = new LinkedHashMap<>();
        activeDefinitions.forEach(definition -> definitionByPath.put(definition.fieldPath(), definition));
        List<String> errors = new ArrayList<>();
        for (String fieldPath : submittedValues.keySet()) {
            ReservationTaskCardFieldDefinition definition = definitionByPath.get(fieldPath);
            if (definition == null) {
                errors.add(fieldPath + ": 不属于当前任务卡、task_subtype 或展示条件。");
                continue;
            }
            if (!isYes(definition.editable())) {
                errors.add(fieldPath + ": 当前字段为只读，不允许人工修改。");
            }
        }
        return errors;
    }

    /**
     * 按字段矩阵执行必填、枚举、日期和数字规则校验。
     */
    private List<String> validateDefinitionValues(
            ReservationTaskSnapshot task,
            List<ReservationTaskCardFieldDefinition> activeDefinitions,
            JsonNode aiPayload,
            Map<String, Object> editedValues,
            Set<String> fieldPathsToValidate,
            boolean validateRequired) {
        return validateDefinitionValues(
                task,
                activeDefinitions,
                aiPayload,
                editedValues,
                fieldPathsToValidate,
                validateRequired,
                false);
    }

    /**
     * 按字段矩阵执行校验。P0 V3 业务事件缺省旧 visible_reason 时做窄口兼容，其他只读必填仍要校验。
     */
    private List<String> validateDefinitionValues(
            ReservationTaskSnapshot task,
            List<ReservationTaskCardFieldDefinition> activeDefinitions,
            JsonNode aiPayload,
            Map<String, Object> editedValues,
            Set<String> fieldPathsToValidate,
            boolean validateRequired,
            boolean allowMissingReadonlyRequired) {
        List<String> errors = new ArrayList<>();
        for (ReservationTaskCardFieldDefinition definition : activeDefinitions) {
            Object value = valueForField(aiPayload, editedValues, definition.fieldPath());
            if (validateRequired && isMissingRequired(task, definition, aiPayload, editedValues, value)) {
                if (allowMissingReadonlyRequired && isReadonlyRequiredMissingAllowed(definition, aiPayload)) {
                    continue;
                }
                errors.add(definition.fieldPath() + ": 必填字段缺失。");
                continue;
            }
            if (!fieldPathsToValidate.contains(definition.fieldPath()) || isEmptyValue(value)) {
                continue;
            }
            errors.addAll(validateEnumOptions(definition, value));
            errors.addAll(validateBasicRule(definition, value));
        }
        return errors;
    }

    /**
     * 兼容 0711 P0 业务事件：V3 message_event 没有旧矩阵根 visible_reason 字段，但保留 relevant_message_excerpt 作为证据。
     */
    private boolean isReadonlyRequiredMissingAllowed(
            ReservationTaskCardFieldDefinition definition,
            JsonNode aiPayload) {
        return !isYes(definition.editable())
                && "visible_reason".equals(definition.fieldPath())
                && V3_CATALOG_CODE.equals(aiPayload.path("catalog_code").asText())
                && aiPayload.path("v3_message_event").isObject();
    }

    /**
     * 生成字段定义路径集合，用于最终确认时校验全部展示字段。
     */
    private Set<String> definitionFieldPaths(List<ReservationTaskCardFieldDefinition> activeDefinitions) {
        Set<String> fieldPaths = new LinkedHashSet<>();
        activeDefinitions.forEach(definition -> fieldPaths.add(definition.fieldPath()));
        return fieldPaths;
    }

    /**
     * 校验入住日期和离店日期的相对顺序。
     */
    private List<String> validateDateRange(
            List<ReservationTaskCardFieldDefinition> activeDefinitions,
            JsonNode aiPayload,
            Map<String, Object> editedValues) {
        if (!hasDefinition(activeDefinitions, "extracted_fields.arrival_date")
                || !hasDefinition(activeDefinitions, "extracted_fields.departure_date")) {
            return List.of();
        }
        Object arrivalValue = valueForField(aiPayload, editedValues, "extracted_fields.arrival_date");
        Object departureValue = valueForField(aiPayload, editedValues, "extracted_fields.departure_date");
        if (isEmptyValue(arrivalValue) || isEmptyValue(departureValue)) {
            return List.of();
        }
        try {
            LocalDate arrivalDate = parseLocalDate(arrivalValue);
            LocalDate departureDate = parseLocalDate(departureValue);
            if (!arrivalDate.isBefore(departureDate)) {
                return List.of("extracted_fields.arrival_date: 入住日期必须早于离店日期。");
            }
        } catch (DateTimeParseException exception) {
            return List.of();
        }
        return List.of();
    }

    /**
     * 判断定义列表里是否包含指定字段。
     */
    private boolean hasDefinition(List<ReservationTaskCardFieldDefinition> definitions, String fieldPath) {
        return definitions.stream().anyMatch(definition -> fieldPath.equals(definition.fieldPath()));
    }

    /**
     * 组装最终确认 payload 的 field_values。用户值优先，缺省时回落到 AI 原始快照。
     */
    private Map<String, Object> buildConfirmedFieldValues(
            List<ReservationTaskCardFieldDefinition> activeDefinitions,
            JsonNode aiPayload,
            Map<String, Object> editedValues) {
        Map<String, Object> confirmedValues = new LinkedHashMap<>();
        for (ReservationTaskCardFieldDefinition definition : activeDefinitions) {
            confirmedValues.put(definition.fieldPath(), valueForField(aiPayload, editedValues, definition.fieldPath()));
        }
        return confirmedValues;
    }

    /**
     * 从草稿或确认 payload 中读取 field_values。
     */
    private Map<String, Object> fieldValuesFromPayloadJson(String payloadJson) {
        Map<String, Object> fieldValues = new LinkedHashMap<>();
        if (trimToNull(payloadJson) == null) {
            return fieldValues;
        }
        JsonNode fieldValuesNode = parseJson(payloadJson).path("field_values");
        if (!fieldValuesNode.isObject()) {
            return fieldValues;
        }
        fieldValuesNode.fields().forEachRemaining(entry ->
                fieldValues.put(entry.getKey(), objectMapper.convertValue(entry.getValue(), Object.class)));
        return fieldValues;
    }

    /**
     * 生成任务卡 payload JSON。field_values 保留矩阵主路径，effective_payload 给后续 OPERA 映射使用。
     */
    private String toPayloadJson(String generatedFrom, Map<String, Object> fieldValues, LocalDateTime now) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("schema_version", PAYLOAD_SCHEMA_VERSION);
        payload.put("generated_from", generatedFrom);
        payload.put("generated_at", now.toString());
        payload.put("field_values", new LinkedHashMap<>(fieldValues));
        payload.put("legacy_field_values", legacyFieldValues(fieldValues));
        payload.put("effective_payload", effectivePayload(fieldValues));
        try {
            return objectMapper.writeValueAsString(payload);
        } catch (Exception exception) {
            throw error(HttpStatus.INTERNAL_SERVER_ERROR, "TASK_PAYLOAD_JSON_INVALID", "任务卡 payload 生成失败。");
        }
    }

    /**
     * 生成旧扁平字段兼容值，供前端过渡读取；主业务逻辑不再依赖这些 key。
     */
    private Map<String, Object> legacyFieldValues(Map<String, Object> fieldValues) {
        Map<String, Object> legacyValues = new LinkedHashMap<>();
        fieldValues.forEach((fieldPath, value) -> {
            String legacyFieldPath = legacyFieldPathFor(fieldPath);
            if (legacyFieldPath != null) {
                legacyValues.put(legacyFieldPath, value);
            }
        });
        return legacyValues;
    }

    /**
     * 按矩阵 field_path 生成嵌套 effective payload，后续 OPERA 参数组装优先消费该结构。
     */
    private Object effectivePayload(Map<String, Object> fieldValues) {
        ObjectNode root = objectMapper.createObjectNode();
        fieldValues.forEach((fieldPath, value) -> setEffectivePayloadValue(root, fieldPath, objectMapper.valueToTree(value)));
        return objectMapper.convertValue(root, Object.class);
    }

    /**
     * 将单个点号 field_path 写入嵌套 JSON，支持 room_items.0 这类显式数组下标。
     */
    private void setEffectivePayloadValue(ObjectNode root, String fieldPath, JsonNode valueNode) {
        if (trimToNull(fieldPath) == null) {
            return;
        }
        setEffectivePayloadValue(root, fieldPath.split("\\."), 0, valueNode);
    }

    /**
     * 递归写入嵌套 effective payload。
     */
    private void setEffectivePayloadValue(JsonNode current, String[] rawParts, int index, JsonNode valueNode) {
        if (index >= rawParts.length) {
            return;
        }
        String part = rawParts[index].replace("[]", "");
        boolean last = index == rawParts.length - 1;
        if (current instanceof ObjectNode objectNode) {
            if (last) {
                objectNode.set(part, valueNode);
                return;
            }
            String nextPart = rawParts[index + 1].replace("[]", "");
            JsonNode child = objectNode.path(part);
            if (child.isMissingNode() || child.isNull() || !matchesExpectedContainer(child, nextPart)) {
                child = isNumericToken(nextPart) ? objectMapper.createArrayNode() : objectMapper.createObjectNode();
                objectNode.set(part, child);
            }
            setEffectivePayloadValue(child, rawParts, index + 1, valueNode);
            return;
        }
        if (current instanceof ArrayNode arrayNode) {
            if (!isNumericToken(part)) {
                return;
            }
            int arrayIndex = Integer.parseInt(part);
            ensureArraySize(arrayNode, arrayIndex);
            if (last) {
                arrayNode.set(arrayIndex, valueNode);
                return;
            }
            String nextPart = rawParts[index + 1].replace("[]", "");
            JsonNode child = arrayNode.get(arrayIndex);
            if (child == null || child.isNull() || !matchesExpectedContainer(child, nextPart)) {
                child = isNumericToken(nextPart) ? objectMapper.createArrayNode() : objectMapper.createObjectNode();
                arrayNode.set(arrayIndex, child);
            }
            setEffectivePayloadValue(child, rawParts, index + 1, valueNode);
        }
    }

    /**
     * 判断当前 JSON 容器是否匹配下一段路径需要的对象或数组。
     */
    private boolean matchesExpectedContainer(JsonNode child, String nextPart) {
        return isNumericToken(nextPart) ? child.isArray() : child.isObject();
    }

    /**
     * 扩展数组到指定下标，缺位填 null。
     */
    private void ensureArraySize(ArrayNode arrayNode, int index) {
        while (arrayNode.size() <= index) {
            arrayNode.addNull();
        }
    }

    /**
     * 将已保存 payload JSON 转为响应对象。空 payload 返回 null，避免前端误认为已有内容。
     */
    private Object jsonPayloadToObject(String payloadJson) {
        if (trimToNull(payloadJson) == null) {
            return null;
        }
        try {
            return objectMapper.readValue(payloadJson, Object.class);
        } catch (Exception exception) {
            throw error(HttpStatus.INTERNAL_SERVER_ERROR, "TASK_PAYLOAD_JSON_INVALID", "任务卡 payload JSON 无效。");
        }
    }

    /**
     * 任务详情顶层复核状态。仅 type-known manual_review 业务任务默认展示 PENDING。
     */
    private String reviewStatusForDetail(ReservationTaskSnapshot task, ReservationTaskCardSnapshot taskCard) {
        if (!AiResultType.MANUAL_REVIEW.code().equals(task.resultType())
                || ReservationSystemTaskType.MANUAL_REVIEW.name().equals(task.systemTaskType())) {
            return null;
        }
        String reviewStatus = trimToNull(taskCard.reviewStatus());
        return reviewStatus == null ? ReservationReviewStatus.PENDING.name() : reviewStatus;
    }

    /**
     * 任务详情透出 AI manual_review 白名单对象，供前端展示缺失字段和人工复核原因。
     */
    private Object manualReviewForDetail(ReservationTaskSnapshot task, ReservationTaskCardSnapshot taskCard) {
        if (!AiResultType.MANUAL_REVIEW.code().equals(task.resultType())) {
            return null;
        }
        JsonNode manualReviewNode = parseJson(taskCard.aiPayloadJson()).path("manual_review");
        if (manualReviewNode.isMissingNode() || manualReviewNode.isNull()) {
            return null;
        }
        return objectMapper.convertValue(manualReviewNode, Object.class);
    }

    /**
     * 只为来源邮件只读任务透出入口结果，兼容旧 S000/S999 和新 S10/S99，不暴露普通任务完整 AI 原始 payload。
     */
    private ReservationSourceMessageOnlyResult buildSourceMessageOnlyResult(
            ReservationTaskSnapshot task,
            ReservationTaskCardSnapshot taskCard) {
        if (!ReservationSystemTaskType.SOURCE_MESSAGE_ONLY.name().equals(task.systemTaskType())) {
            return null;
        }
        JsonNode payload = parseJson(taskCard.aiPayloadJson());
        return new ReservationSourceMessageOnlyResult(
                textValue(payload, "entry_result_code"),
                textValue(payload, "entry_result_meaning"),
                textValue(payload, "entry_result_description"),
                textValue(payload, "source_message_id"),
                textValue(payload, "result_type"),
                textValue(payload, "route_code"),
                rawAnswerField(payload, "agent_assessment"),
                rawAnswerField(payload, "notification"),
                rawAnswerField(payload, "manual_review"),
                textValue(payload, "raw_answer"));
    }

    /**
     * 查询当前任务对应的 AI transition，用于详情展示 V3 route 和系统处理分类。
     */
    private ReservationAiTransitionSnapshot findTaskTransition(ReservationTaskSnapshot task) {
        return workflowRepository.findTransitionById(task.hotelId(), task.aiTransitionId()).orElse(null);
    }

    /**
     * 查询同一 AI 批次下的关联 transition，用于展示没有生成任务的诊断类结果。
     */
    private List<ReservationAiTransitionSnapshot> findRelatedTransitions(
            ReservationTaskSnapshot task,
            ReservationAiTransitionSnapshot transition) {
        if (transition == null) {
            return List.of();
        }
        return workflowRepository.findTransitionsByBatchId(task.hotelId(), transition.batchId());
    }

    /**
     * 按系统处理分类构造详情页展示块。
     */
    private List<ReservationAiTransitionDisplayResult> buildTransitionDisplayResults(
            List<ReservationAiTransitionSnapshot> transitions,
            ReservationAiSystemProcessCategory category) {
        if (transitions == null || transitions.isEmpty() || category == null) {
            return List.of();
        }
        return transitions.stream()
                .filter(transition -> category.name().equals(transition.systemProcessCategory()))
                .map(this::toTransitionDisplayResult)
                .toList();
    }

    /**
     * 转换 AI transition 快照为前端详情展示块。
     */
    private ReservationAiTransitionDisplayResult toTransitionDisplayResult(ReservationAiTransitionSnapshot transition) {
        return new ReservationAiTransitionDisplayResult(
                transition.id().toString(),
                transition.sourceEventIndex(),
                transition.arrayIndex(),
                transition.resultType(),
                transition.aiTaskType(),
                transition.taskSubtype(),
                transition.routeCode(),
                transition.systemProcessCategory(),
                transition.adapterErrorCode(),
                transition.adapterErrorMessage(),
                diagnosticPayloadFragment(transition.aiPayloadJson()));
    }

    /**
     * 从 AI item 中提取前端可展示的诊断白名单字段，避免泄露完整业务字段、附件和上下文。
     */
    private Object diagnosticPayloadFragment(String aiPayloadJson) {
        if (trimToNull(aiPayloadJson) == null) {
            return null;
        }
        JsonNode payload = parseJson(aiPayloadJson);
        Map<String, Object> fragment = new LinkedHashMap<>();
        copyDiagnosticPayloadField(payload, fragment, "event_type");
        copyDiagnosticPayloadField(payload, fragment, "source_event_index");
        copyDiagnosticPayloadField(payload, fragment, "current_or_history");
        copyDiagnosticPayloadField(payload, fragment, "contract_errors");
        copyDiagnosticPayloadField(payload, fragment, "intent_type");
        copyDiagnosticPayloadField(payload, fragment, "reason_code");
        copyDiagnosticPayloadField(payload, fragment, "missing_fields");
        copyDiagnosticPayloadField(payload, fragment, "parent_source_event_index");
        copyDiagnosticPayloadField(payload, fragment, "linked_task_group_id");
        copyDiagnosticPayloadField(payload, fragment, "blocked_until_parent_completed");
        return fragment;
    }

    /**
     * 复制单个诊断字段到展示片段，忽略空值和不存在字段。
     */
    private void copyDiagnosticPayloadField(JsonNode payload, Map<String, Object> fragment, String fieldName) {
        JsonNode value = payload == null ? null : payload.get(fieldName);
        if (value == null || value.isNull() || value.isMissingNode()) {
            return;
        }
        fragment.put(fieldName, objectMapper.convertValue(value, Object.class));
    }

    /**
     * 从 S10/S99 原始回答中提取指定对象字段；旧 S000/S999 文本无法解析时返回 null。
     */
    private Object rawAnswerField(JsonNode payload, String fieldName) {
        String rawAnswer = textValue(payload, "raw_answer");
        if (trimToNull(rawAnswer) == null || trimToNull(fieldName) == null) {
            return null;
        }
        try {
            JsonNode root = objectMapper.readTree(rawAnswer);
            JsonNode fieldNode = root.path(fieldName);
            if (fieldNode.isMissingNode() || fieldNode.isNull()) {
                return null;
            }
            return objectMapper.convertValue(fieldNode, Object.class);
        } catch (Exception exception) {
            return null;
        }
    }

    /**
     * 写入草稿保存或最终确认审计。审计只记录 payload 摘要，不保存完整客户字段。
     */
    private void writePayloadMutationAudit(
            ReservationTaskSnapshot task,
            String action,
            String beforePayloadJson,
            String afterPayloadJson,
            LocalDateTime now) {
        workflowRepository.insertAuditLog(new ReservationAuditLogDraft(
                task.hotelId(),
                task.orderId(),
                task.id(),
                null,
                ACTOR_TYPE_USER,
                ACTOR_ID_LOCAL,
                action,
                null,
                payloadAuditSnapshotJson(beforePayloadJson),
                payloadAuditSnapshotJson(afterPayloadJson),
                now));
    }

    /**
     * 生成 payload 审计摘要，避免审计表保存完整用户字段值。
     */
    private String payloadAuditSnapshotJson(String payloadJson) {
        Map<String, Object> snapshot = new LinkedHashMap<>();
        Map<String, Object> fieldValues = fieldValuesFromPayloadJson(payloadJson);
        snapshot.put("has_payload", trimToNull(payloadJson) != null);
        snapshot.put("schema_version", trimToNull(payloadJson) == null ? null : PAYLOAD_SCHEMA_VERSION);
        snapshot.put("field_count", fieldValues.size());
        return toJson(snapshot);
    }

    /**
     * 判断矩阵中的“是/否”开关。
     */
    private boolean isYes(String value) {
        return "是".equals(trimToNull(value));
    }

    /**
     * 判断矩阵枚举式文本是否匹配当前任务值，支持斜杠分隔的多个 subtype。
     */
    private boolean matchesMatrixExpression(String matrixValue, String actualValue) {
        String normalizedMatrixValue = trimToNull(matrixValue);
        if (normalizedMatrixValue == null || "-".equals(normalizedMatrixValue)) {
            return true;
        }
        String normalizedActualValue = trimToNull(actualValue);
        if (normalizedActualValue == null) {
            return false;
        }
        String[] candidates = normalizedMatrixValue
                .replace("／", "/")
                .replace("，", "/")
                .replace(",", "/")
                .replace("、", "/")
                .replace("\n", "/")
                .split("/");
        for (String candidate : candidates) {
            String normalizedCandidate = trimToNull(candidate);
            if (normalizedCandidate == null) {
                continue;
            }
            if (normalizedCandidate.equals(normalizedActualValue)
                    || normalizedCandidate.contains(normalizedActualValue)) {
                return true;
            }
        }
        return false;
    }

    /**
     * type-known manual_review 在原业务任务卡上复核，因此字段矩阵复用 normal_task 的业务卡规则。
     */
    private boolean matchesTaskResultType(String matrixResultType, ReservationTaskSnapshot task) {
        if (AiResultType.MANUAL_REVIEW.code().equals(task.resultType())
                && !ReservationSystemTaskType.MANUAL_REVIEW.name().equals(task.systemTaskType())
                && matchesMatrixExpression(matrixResultType, AiResultType.NORMAL_TASK.code())) {
            return true;
        }
        return matchesMatrixExpression(matrixResultType, task.resultType());
    }

    /**
     * 按当前已知矩阵展示条件判断字段是否参与本次后端校验。
     */
    private boolean displayConditionMatches(
            ReservationTaskSnapshot task,
            ReservationTaskCardFieldDefinition definition,
            JsonNode aiPayload,
            Map<String, Object> editedValues) {
        String condition = trimToNull(definition.displayCondition());
        if (condition == null || "-".equals(condition) || condition.contains("始终展示")) {
            return true;
        }
        Object currentValue = valueForField(aiPayload, editedValues, definition.fieldPath());
        if (condition.contains("有值时展示")) {
            return !isEmptyValue(currentValue);
        }
        if (condition.contains("有附件")) {
            return !isEmptyValue(valueForField(aiPayload, editedValues, "attachments"));
        }
        if (condition.contains("对象为FIT Reservation")) {
            return isFitTarget(task, aiPayload, editedValues);
        }
        if (condition.contains("对象为Group Block或Allotment")) {
            return isGroupOrAllotmentTarget(task, aiPayload, editedValues);
        }
        if (condition.contains("涉及房型")) {
            return ReservationSystemTaskType.NEW_BOOKING.name().equals(task.systemTaskType())
                    || !isEmptyValue(valueForField(aiPayload, editedValues, "extracted_fields.room_type"))
                    || !isEmptyValue(valueForField(aiPayload, editedValues, "extracted_fields.pms_room_type_code"));
        }
        if (condition.contains("涉及Rate Code") || condition.contains("涉及价格/Rate Code")) {
            return !isEmptyValue(valueForField(aiPayload, editedValues, "extracted_fields.rate_code_result"))
                    || subtypeContains(task, "manual_rate_code_or_settlement_price");
        }
        if (condition.contains("需要价格确认")) {
            return !isEmptyValue(valueForField(aiPayload, editedValues,
                    "extracted_fields.rate_code_result.settlement_price"));
        }
        if (condition.contains("有复合单价") || condition.contains("fix_charge_required=true")) {
            return isBooleanTrue(valueForField(aiPayload, editedValues, "extracted_fields.fix_charge_required"))
                    || !isEmptyValue(valueForField(aiPayload, editedValues, "extracted_fields.fix_charge_items[]"));
        }
        if (condition.contains("allocation_split_from_parent=true")) {
            return isBooleanTrue(valueForField(aiPayload, editedValues,
                    "extracted_fields.allocation_split_from_parent"));
        }
        if (condition.contains("来自QBD/LianTai表格")) {
            return !isEmptyValue(valueForField(aiPayload, editedValues, "extracted_fields.table_evidence"));
        }
        if (condition.contains("有Group目标")) {
            return !isEmptyValue(valueForField(aiPayload, editedValues, "case_keys.group_code"))
                    || isGroupOrAllotmentTarget(task, aiPayload, editedValues);
        }
        if (condition.contains("有FIT目标") || condition.contains("FIT取消")) {
            return !isEmptyValue(valueForField(aiPayload, editedValues, "case_keys.confirmation_number"))
                    || isFitTarget(task, aiPayload, editedValues);
        }
        if (condition.contains("有before/after修改")) {
            return !isEmptyValue(valueForField(aiPayload, editedValues, "extracted_fields.before_after"));
        }
        if (condition.contains("update_fix_charge")) {
            return subtypeContains(task, "update_fix_charge")
                    || isBooleanTrue(valueForField(aiPayload, editedValues, "extracted_fields.fix_charge_required"));
        }
        return true;
    }

    /**
     * 判断字段是否缺失必填值，并处理“可使用另一个业务号”和“New Booking 后续生成订单号”的例外。
     */
    private boolean isMissingRequired(
            ReservationTaskSnapshot task,
            ReservationTaskCardFieldDefinition definition,
            JsonNode aiPayload,
            Map<String, Object> editedValues,
            Object value) {
        if (!isRequired(definition)) {
            return false;
        }
        if (!isEmptyValue(value)) {
            return false;
        }
        if ("case_keys.group_code".equals(definition.fieldPath())
                && !isEmptyValue(valueForField(aiPayload, editedValues, "case_keys.confirmation_number"))) {
            return false;
        }
        if ("case_keys.confirmation_number".equals(definition.fieldPath())) {
            if (!isEmptyValue(valueForField(aiPayload, editedValues, "case_keys.group_code"))) {
                return false;
            }
            if (ReservationSystemTaskType.NEW_BOOKING.name().equals(task.systemTaskType())
                    && containsText(definition.validationRule(), "系统生成")) {
                return false;
            }
        }
        return true;
    }

    /**
     * 判断矩阵必填规则。条件必填是否生效已通过展示条件筛选决定。
     */
    private boolean isRequired(ReservationTaskCardFieldDefinition definition) {
        String requiredRule = trimToNull(definition.requiredRule());
        if (requiredRule == null || "否".equals(requiredRule)) {
            return false;
        }
        return "是".equals(requiredRule)
                || requiredRule.contains("条件必填")
                || requiredRule.contains("表格来源必填");
    }

    /**
     * 校验字段枚举值。对象型字段和 key=value 复合枚举先不做强校验。
     */
    private List<String> validateEnumOptions(ReservationTaskCardFieldDefinition definition, Object value) {
        String enumOptions = trimToNull(definition.enumOptions());
        if (enumOptions == null || "-".equals(enumOptions) || enumOptions.contains("=") || value instanceof Map) {
            return List.of();
        }
        Set<String> allowedOptions = parseEnumOptions(enumOptions);
        if (allowedOptions.isEmpty()) {
            return List.of();
        }
        List<String> errors = new ArrayList<>();
        if (value instanceof Iterable<?> iterableValue) {
            for (Object item : iterableValue) {
                if (!allowedOptions.contains(String.valueOf(item))) {
                    errors.add(definition.fieldPath() + ": 枚举值不在矩阵允许范围内。");
                    break;
                }
            }
            return errors;
        }
        if (!allowedOptions.contains(String.valueOf(value))) {
            errors.add(definition.fieldPath() + ": 枚举值不在矩阵允许范围内。");
        }
        return errors;
    }

    /**
     * 解析矩阵枚举选项，兼容斜杠、分号、逗号和中文括号说明。
     */
    private Set<String> parseEnumOptions(String enumOptions) {
        Set<String> allowedOptions = new LinkedHashSet<>();
        String[] tokens = enumOptions
                .replace("／", "/")
                .replace("；", "/")
                .replace(";", "/")
                .replace("，", "/")
                .replace(",", "/")
                .replace("\n", "/")
                .split("/");
        for (String token : tokens) {
            String normalizedToken = trimToNull(token);
            if (normalizedToken == null) {
                continue;
            }
            int chineseNoteIndex = normalizedToken.indexOf('（');
            int englishNoteIndex = normalizedToken.indexOf('(');
            int noteIndex = chineseNoteIndex >= 0 ? chineseNoteIndex : englishNoteIndex;
            if (noteIndex >= 0) {
                normalizedToken = trimToNull(normalizedToken.substring(0, noteIndex));
            }
            if (normalizedToken != null) {
                allowedOptions.add(normalizedToken);
            }
        }
        return allowedOptions;
    }

    /**
     * 校验矩阵中的基础日期、数字和正整数规则。
     */
    private List<String> validateBasicRule(ReservationTaskCardFieldDefinition definition, Object value) {
        List<String> errors = new ArrayList<>();
        String validationRule = trimToNull(definition.validationRule());
        String fieldPath = definition.fieldPath();
        if (isDateField(definition)) {
            try {
                parseLocalDate(value);
            } catch (DateTimeParseException exception) {
                errors.add(fieldPath + ": 日期格式必须为 yyyy-MM-dd。");
            }
        }
        if (isNumberField(definition)) {
            BigDecimal numberValue = parseBigDecimal(value);
            if (numberValue == null) {
                errors.add(fieldPath + ": 必须是数字。");
            } else if (containsText(validationRule, "正整数")
                    && (numberValue.scale() > 0 || numberValue.compareTo(BigDecimal.ZERO) <= 0)) {
                errors.add(fieldPath + ": 必须是正整数。");
            } else if (containsText(validationRule, "非负") && numberValue.compareTo(BigDecimal.ZERO) < 0) {
                errors.add(fieldPath + ": 必须是非负数。");
            }
        }
        return errors;
    }

    /**
     * 判断字段是否是日期型字段。
     */
    private boolean isDateField(ReservationTaskCardFieldDefinition definition) {
        return isYes(definition.datePicker())
                || containsText(definition.validationRule(), "日期")
                || definition.fieldPath().endsWith("_date");
    }

    /**
     * 判断字段是否是数字型字段。
     */
    private boolean isNumberField(ReservationTaskCardFieldDefinition definition) {
        return isYes(definition.numberInput())
                || containsText(definition.validationRule(), "正整数")
                || containsText(definition.validationRule(), "非负数");
    }

    /**
     * 从用户编辑值或 AI 原始快照中读取字段值，用户编辑值优先。
     */
    private Object valueForField(JsonNode aiPayload, Map<String, Object> editedValues, String fieldPath) {
        if (editedValues.containsKey(fieldPath)) {
            return editedValues.get(fieldPath);
        }
        for (String aliasPath : p0ValuePathCandidates(fieldPath)) {
            if (editedValues.containsKey(aliasPath)) {
                return editedValues.get(aliasPath);
            }
        }
        Object value = valueAt(aiPayload, fieldPath);
        if (value != null) {
            return value;
        }
        for (String legacyPath : p0ValuePathCandidates(fieldPath)) {
            Object legacyValue = valueAt(aiPayload, legacyPath);
            if (legacyValue != null) {
                return legacyValue;
            }
        }
        return null;
    }

    /**
     * 0711 P0 已将房型字段迁移到 room_items[]；读取时保留旧扁平字段双向兼容。
     */
    private List<String> p0ValuePathCandidates(String fieldPath) {
        String safeFieldPath = fieldPath == null ? "" : fieldPath;
        return switch (safeFieldPath) {
            case "extracted_fields.room_type" -> List.of(
                    "extracted_fields.room_items.0.room_type_normalized",
                    "extracted_fields.room_items.0.room_type_raw");
            case "extracted_fields.room_quantity" -> List.of("extracted_fields.room_items.0.room_quantity");
            case "extracted_fields.pms_room_type_code" -> List.of(
                    "extracted_fields.room_items.0.pms_room_type_code");
            case "extracted_fields.room_items.0.room_type_raw" -> List.of(
                    "extracted_fields.room_type",
                    "extracted_fields.room_items.0.room_type_normalized");
            case "extracted_fields.room_items.0.room_quantity" -> List.of("extracted_fields.room_quantity");
            case "extracted_fields.room_items.0.pms_room_type_code" -> List.of(
                    "extracted_fields.pms_room_type_code");
            default -> List.of();
        };
    }

    /**
     * 旧扁平字段转 P0 room_items[0] 主字段路径。未迁移字段返回 null。
     */
    private String canonicalP0RoomItemFieldPath(String fieldPath) {
        return switch (fieldPath == null ? "" : fieldPath) {
            case "extracted_fields.room_quantity" -> "extracted_fields.room_items.0.room_quantity";
            case "extracted_fields.room_type" -> "extracted_fields.room_items.0.room_type_raw";
            case "extracted_fields.pms_room_type_code" -> "extracted_fields.room_items.0.pms_room_type_code";
            default -> null;
        };
    }

    /**
     * P0 room_items[0] 主字段转旧扁平字段路径，用于前端兼容显示。
     */
    private String legacyFieldPathFor(String fieldPath) {
        return switch (fieldPath == null ? "" : fieldPath) {
            case "extracted_fields.room_items.0.room_quantity" -> "extracted_fields.room_quantity";
            case "extracted_fields.room_items.0.room_type_raw" -> "extracted_fields.room_type";
            case "extracted_fields.room_items.0.pms_room_type_code" -> "extracted_fields.pms_room_type_code";
            default -> null;
        };
    }

    /**
     * 将矩阵 field_path 转为 RFC 6901 JSON Pointer，供前端编辑和复核定位。
     */
    private String fieldPointerFor(String fieldPath) {
        String normalized = trimToNull(fieldPath);
        if (normalized == null) {
            return null;
        }
        StringBuilder pointer = new StringBuilder();
        for (String rawPart : normalized.split("\\.")) {
            String part = rawPart.replace("[]", "");
            if (part.isBlank()) {
                continue;
            }
            pointer.append('/').append(escapeJsonPointerToken(part));
        }
        return pointer.isEmpty() ? null : pointer.toString();
    }

    /**
     * 转义 JSON Pointer token。
     */
    private String escapeJsonPointerToken(String token) {
        return token.replace("~", "~0").replace("/", "~1");
    }

    /**
     * 判断值是否为空。false 和 0 是有效值，不当作空。
     */
    private boolean isEmptyValue(Object value) {
        if (value == null) {
            return true;
        }
        if (value instanceof String stringValue) {
            return stringValue.trim().isEmpty();
        }
        if (value instanceof Iterable<?> iterableValue) {
            return !iterableValue.iterator().hasNext();
        }
        if (value instanceof Map<?, ?> mapValue) {
            return mapValue.isEmpty();
        }
        return false;
    }

    /**
     * 判断值是否是布尔 true。
     */
    private boolean isBooleanTrue(Object value) {
        if (value instanceof Boolean booleanValue) {
            return booleanValue;
        }
        if (value instanceof String stringValue) {
            return "true".equalsIgnoreCase(stringValue.trim());
        }
        return false;
    }

    /**
     * 判断当前任务是否是 FIT 目标。
     */
    private boolean isFitTarget(
            ReservationTaskSnapshot task,
            JsonNode aiPayload,
            Map<String, Object> editedValues) {
        Object bookingObjectType = valueForField(aiPayload, editedValues, "extracted_fields.booking_object_type");
        return "FIT Reservation".equals(String.valueOf(bookingObjectType)) || subtypeContains(task, "fit");
    }

    /**
     * 判断当前任务是否是 Group 或 Allotment 目标。
     */
    private boolean isGroupOrAllotmentTarget(
            ReservationTaskSnapshot task,
            JsonNode aiPayload,
            Map<String, Object> editedValues) {
        Object bookingObjectType = valueForField(aiPayload, editedValues, "extracted_fields.booking_object_type");
        String objectTypeText = String.valueOf(bookingObjectType);
        return objectTypeText.contains("Group")
                || objectTypeText.contains("Allotment")
                || objectTypeText.contains("Control")
                || subtypeContains(task, "group")
                || subtypeContains(task, "allotment")
                || subtypeContains(task, "control");
    }

    /**
     * 判断 task_subtype 是否包含指定片段。
     */
    private boolean subtypeContains(ReservationTaskSnapshot task, String keyword) {
        String taskSubtype = trimToNull(task.taskSubtype());
        return taskSubtype != null && taskSubtype.contains(keyword);
    }

    /**
     * 解析日期值。
     */
    private LocalDate parseLocalDate(Object value) {
        return LocalDate.parse(String.valueOf(value));
    }

    /**
     * 解析数字值；无法解析时返回 null，让调用方生成字段错误。
     */
    private BigDecimal parseBigDecimal(Object value) {
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (NumberFormatException exception) {
            return null;
        }
    }

    /**
     * 判断文本中是否包含片段，兼容空值。
     */
    private boolean containsText(String text, String keyword) {
        return text != null && text.contains(keyword);
    }

    /**
     * 根据任务卡矩阵和 AI 原始快照组装详情字段。
     */
    private List<ReservationTaskFieldResult> buildFieldResults(
            ReservationTaskSnapshot task,
            ReservationTaskCardSnapshot taskCard) {
        JsonNode aiPayload = parseJson(taskCard.aiPayloadJson());
        return fieldDefinitionProvider.listDefinitions(task.taskCardType(), task.resultType())
                .stream()
                .map(definition -> toFieldResult(definition, aiPayload))
                .toList();
    }

    /**
     * 查询任务详情顶部来源消息会话摘要。只读取安全摘要，不读取正文或附件 URL。
     */
    private SourceMessageDetailContext findSourceMessageDetailContext(ReservationTaskSnapshot task) {
        SourceMessageSummaryResponse summary = sourceMessageQueryService.getSummary(task.sourceMessageId())
                .orElse(null);
        if (summary == null) {
            return new SourceMessageDetailContext(null, null, null, null, null);
        }
        Long conversationMessageCount = null;
        if (trimToNull(summary.externalConversationId()) != null) {
            conversationMessageCount = sourceMessageQueryService.countByExternalConversationIds(
                            task.hotelId(),
                            List.of(summary.externalConversationId()))
                    .get(summary.externalConversationId());
        }
        return new SourceMessageDetailContext(
                summary.subject(),
                summary.senderSummary(),
                summary.receivedAt(),
                summary.externalConversationId(),
                conversationMessageCount);
    }

    /**
     * 将字段定义转换为详情字段结果，并从 AI payload 中读取当前值。
     */
    private ReservationTaskFieldResult toFieldResult(ReservationTaskCardFieldDefinition definition, JsonNode aiPayload) {
        return new ReservationTaskFieldResult(
                definition.rowNumber(),
                definition.cardName(),
                definition.resultType(),
                definition.taskType(),
                definition.taskSubtype(),
                definition.displayArea(),
                definition.fieldPath(),
                fieldPointerFor(definition.fieldPath()),
                legacyFieldPathFor(definition.fieldPath()),
                definition.displayName(),
                definition.visible(),
                definition.editable(),
                definition.inputEditable(),
                definition.selectEditable(),
                definition.datePicker(),
                definition.numberInput(),
                definition.fileDisplay(),
                definition.tableEditable(),
                definition.enumOptions(),
                definition.requiredRule(),
                definition.defaultValueSource(),
                definition.displayCondition(),
                definition.validationRule(),
                definition.writePath(),
                definition.operaWriteParticipation(),
                definition.operaParameterMapping(),
                definition.notes(),
                valueForField(aiPayload, Map.of(), definition.fieldPath()));
    }

    /**
     * 任务详情来源消息展示上下文。只包含 SourceMessage Inbox 安全摘要字段。
     */
    private record SourceMessageDetailContext(
            String subject,
            String senderSummary,
            OffsetDateTime receivedAt,
            String externalConversationId,
            Long conversationMessageCount) {
    }

    /**
     * 从 JSON 中按简单点路径读取值，支持矩阵中的数组路径后缀。
     */
    private Object valueAt(JsonNode root, String fieldPath) {
        if (root == null || fieldPath == null || fieldPath.isBlank()) {
            return null;
        }
        JsonNode current = root;
        for (String rawPart : fieldPath.split("\\.")) {
            String part = rawPart.replace("[]", "");
            current = current.isArray() && isNumericToken(part)
                    ? current.path(Integer.parseInt(part))
                    : current.path(part);
            if (current.isMissingNode() || current.isNull()) {
                return null;
            }
        }
        return objectMapper.convertValue(current, Object.class);
    }

    /**
     * 解析任务卡 AI JSON 快照。
     */
    private JsonNode parseJson(String json) {
        try {
            return objectMapper.readTree(json == null ? "{}" : json);
        } catch (Exception exception) {
            throw error(HttpStatus.INTERNAL_SERVER_ERROR, "TASK_CARD_JSON_INVALID", "任务卡 JSON 无效。");
        }
    }

    /**
     * 从 JSON 节点安全读取文本字段，缺失或 null 时返回 null。
     */
    private String textValue(JsonNode node, String fieldName) {
        if (node == null || fieldName == null) {
            return null;
        }
        JsonNode valueNode = node.path(fieldName);
        if (valueNode.isMissingNode() || valueNode.isNull()) {
            return null;
        }
        return valueNode.asText();
    }

    /**
     * 解析人工转换目标类型，只允许 New、Update 和 Cancel。
     */
    private TargetTaskType parseTargetTaskType(String rawTargetTaskType) {
        String targetTaskType = trimToNull(rawTargetTaskType);
        if (targetTaskType == null) {
            throw error(HttpStatus.BAD_REQUEST, "TARGET_TASK_TYPE_REQUIRED", "target_task_type 必填。");
        }
        return switch (targetTaskType) {
            case "NEW_BOOKING" -> new TargetTaskType(
                    ReservationSystemTaskType.NEW_BOOKING,
                    ReservationTaskCardType.NEW_BOOKING,
                    "New Booking");
            case "UPDATE_BOOKING" -> new TargetTaskType(
                    ReservationSystemTaskType.UPDATE_BOOKING,
                    ReservationTaskCardType.UPDATE_BOOKING,
                    "Update Booking");
            case "CANCEL_BOOKING" -> new TargetTaskType(
                    ReservationSystemTaskType.CANCEL_BOOKING,
                    ReservationTaskCardType.CANCEL_BOOKING,
                    "Cancel Booking");
            default -> throw error(HttpStatus.BAD_REQUEST, "TARGET_TASK_TYPE_UNSUPPORTED", "target_task_type 不支持。");
        };
    }

    /**
     * 解析转换后的目标订单。Update / Cancel 必须显式选择目标订单。
     */
    private ReservationOrderSnapshot resolveTargetOrder(
            ReservationTaskSnapshot task,
            ReservationOrderSnapshot originalOrder,
            TargetTaskType targetTaskType,
            ManualReviewConversionRequest request) {
        if (targetTaskType.systemTaskType() == ReservationSystemTaskType.NEW_BOOKING) {
            return resolveNewBookingTargetOrder(task, originalOrder);
        }
        String rawTargetOrderId = request == null ? null : trimToNull(request.targetOrderId());
        if (rawTargetOrderId == null) {
            throw error(HttpStatus.BAD_REQUEST, "TARGET_ORDER_REQUIRED", "转为 Update 或 Cancel 时 target_order_id 必填。");
        }
        Long targetOrderId = parseLong(rawTargetOrderId, "TARGET_ORDER_INVALID", "target_order_id 格式无效。");
        ReservationOrderSnapshot targetOrder = workflowRepository.findOrderById(task.hotelId(), targetOrderId)
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "TARGET_ORDER_NOT_FOUND", "目标订单不存在。"));
        if (ReservationOrderStatus.LOGIC_DELETED.name().equals(targetOrder.orderStatus())) {
            throw error(HttpStatus.BAD_REQUEST, "TARGET_ORDER_INVALID", "目标订单已逻辑删除。");
        }
        return targetOrder;
    }

    /**
     * 解析 Fallback 转 New Booking 的目标订单。已有业务号时优先激活原临时订单，不等待 OPERA 模拟回填。
     */
    private ReservationOrderSnapshot resolveNewBookingTargetOrder(
            ReservationTaskSnapshot task,
            ReservationOrderSnapshot originalOrder) {
        if (!ReservationOrderStatus.TEMPORARY.name().equals(originalOrder.orderStatus())) {
            return originalOrder;
        }
        BusinessKey businessKey = selectBusinessKeyFromTaskCard(task);
        if (businessKey == null) {
            return originalOrder;
        }
        ReservationOrderSnapshot existingActiveOrder = workflowRepository
                .findActiveOrderByBusinessKey(task.hotelId(), businessKey.keyType().name(), businessKey.value())
                .orElse(null);
        if (existingActiveOrder != null && !existingActiveOrder.id().equals(originalOrder.id())) {
            throw error(HttpStatus.CONFLICT, "ORDER_ACTIVE_KEY_CONFLICT", "业务号已存在 ACTIVE 订单，请选择 Update 或 Cancel。");
        }
        try {
            boolean updated = workflowRepository.activateTemporaryOrderWithBusinessKey(
                    task.hotelId(),
                    originalOrder.id(),
                    businessKey.keyType().name(),
                    businessKey.value(),
                    ReservationBusinessKeySource.AI_CANDIDATE.name(),
                    nowUtc());
            if (!updated) {
                return workflowRepository.findOrderById(task.hotelId(), originalOrder.id())
                        .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "ORDER_NOT_FOUND", "原订单不存在。"));
            }
            return workflowRepository.findOrderById(task.hotelId(), originalOrder.id())
                    .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "ORDER_NOT_FOUND", "原订单不存在。"));
        } catch (DuplicateKeyException exception) {
            throw error(HttpStatus.CONFLICT, "ORDER_ACTIVE_KEY_CONFLICT", "业务号已存在 ACTIVE 订单，请选择 Update 或 Cancel。");
        }
    }

    /**
     * 从任务卡 AI 原始 JSON 中读取订单业务号候选，当前只使用 case_keys 中的稳定路径。
     */
    private BusinessKey selectBusinessKeyFromTaskCard(ReservationTaskSnapshot task) {
        ReservationTaskCardSnapshot taskCard = workflowRepository
                .findTaskCardByTaskId(task.hotelId(), task.id())
                .orElseThrow(() -> error(HttpStatus.NOT_FOUND, "TASK_CARD_NOT_FOUND", "任务卡不存在。"));
        JsonNode aiPayload = parseJson(taskCard.aiPayloadJson());
        JsonNode caseKeys = aiPayload.path("case_keys");
        String groupCode = trimToNull(caseKeys.path("group_code").asText(null));
        if (groupCode != null) {
            return new BusinessKey(ReservationOrderKeyType.GROUP_CODE, groupCode);
        }
        String confirmationNumber = trimToNull(caseKeys.path("confirmation_number").asText(null));
        if (confirmationNumber != null) {
            return new BusinessKey(ReservationOrderKeyType.CONFIRMATION_NUMBER, confirmationNumber);
        }
        return null;
    }

    /**
     * 任务迁移后，如果原临时订单已经没有任务，则逻辑删除原订单。
     */
    private boolean logicDeleteOriginalTemporaryOrderIfEmpty(
            ReservationTaskSnapshot task,
            ReservationOrderSnapshot originalOrder,
            ReservationOrderSnapshot targetOrder,
            LocalDateTime now) {
        if (originalOrder.id().equals(targetOrder.id())) {
            return false;
        }
        if (!ReservationOrderStatus.TEMPORARY.name().equals(originalOrder.orderStatus())) {
            return false;
        }
        if (workflowRepository.countTasksByOrder(task.hotelId(), originalOrder.id()) > 0) {
            return false;
        }
        return workflowRepository.logicDeleteOrder(
                task.hotelId(),
                originalOrder.id(),
                "人工复核任务已迁移到目标订单。",
                now);
    }

    /**
     * 写入人工转换审计，记录前后摘要和可选原因。
     */
    private void writeManualConversionAudit(
            ReservationTaskSnapshot task,
            ReservationOrderSnapshot originalOrder,
            ReservationOrderSnapshot targetOrder,
            TargetTaskType targetTaskType,
            ManualReviewConversionRequest request,
            boolean originalOrderLogicDeleted,
            LocalDateTime now) {
        workflowRepository.insertAuditLog(new ReservationAuditLogDraft(
                task.hotelId(),
                targetOrder.id(),
                task.id(),
                null,
                ACTOR_TYPE_USER,
                ACTOR_ID_LOCAL,
                ACTION_MANUAL_REVIEW_CONVERT,
                request == null ? null : trimToNull(request.reason()),
                toJson(Map.of(
                        "order_id", task.orderId().toString(),
                        "system_task_type", task.systemTaskType(),
                        "task_card_type", task.taskCardType())),
                toJson(Map.of(
                        "order_id", targetOrder.id().toString(),
                        "system_task_type", targetTaskType.systemTaskType().name(),
                        "task_card_type", targetTaskType.taskCardType().name(),
                        "original_order_id", originalOrder.id().toString(),
                        "original_order_logic_deleted", originalOrderLogicDeleted)),
                now));
    }

    /**
     * 将审计摘要转换为 JSON 字符串。
     */
    private String toJson(Map<String, Object> snapshot) {
        try {
            return objectMapper.writeValueAsString(snapshot);
        } catch (Exception exception) {
            throw error(HttpStatus.INTERNAL_SERVER_ERROR, "AUDIT_JSON_INVALID", "审计摘要生成失败。");
        }
    }

    /**
     * 解析 Long ID，避免无效 ID 进入 Repository。
     */
    private Long parseLong(String rawValue, String errorCode, String message) {
        try {
            return Long.valueOf(rawValue);
        } catch (NumberFormatException exception) {
            throw error(HttpStatus.BAD_REQUEST, errorCode, message);
        }
    }

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

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

    /**
     * 构建任务工作流受控异常。
     */
    private ReservationTaskWorkflowException error(HttpStatus status, String errorCode, String message) {
        return new ReservationTaskWorkflowException(status, errorCode, message);
    }

    /**
     * 构建字段矩阵校验失败异常，details 只包含字段路径和规则原因。
     */
    private ReservationTaskWorkflowException validationError(List<String> details) {
        return new ReservationTaskWorkflowException(
                HttpStatus.BAD_REQUEST,
                "TASK_FIELD_VALIDATION_FAILED",
                "任务字段校验失败。",
                details);
    }

    /**
     * 人工转换目标类型映射。
     */
    private record TargetTaskType(
            ReservationSystemTaskType systemTaskType,
            ReservationTaskCardType taskCardType,
            String aiTaskType
    ) {
    }

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