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

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.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.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.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.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.ReservationOperaOperationAttemptResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationResult;
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 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_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_path-v1";
    private static final String PAYLOAD_SOURCE_DRAFT = "TASK_DRAFT";
    private static final String PAYLOAD_SOURCE_CONFIRMATION = "TASK_CONFIRMATION";
    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);
        SourceMessageDetailContext sourceContext = findSourceMessageDetailContext(task);
        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.taskStatus(),
                taskCard.fieldContractVersion(),
                jsonPayloadToObject(taskCard.draftPayloadJson()),
                jsonPayloadToObject(taskCard.confirmedPayloadJson()),
                availability,
                fields,
                operaOperations);
    }

    /**
     * 将 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());
        Map<String, Object> submittedValues = normalizeFieldValues(request == null ? null : request.fieldValues());
        Map<String, Object> draftValues = 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);

        JsonNode aiPayload = parseJson(taskCard.aiPayloadJson());
        Map<String, Object> submittedValues = normalizeFieldValues(request == null ? null : request.fieldValues());
        Map<String, Object> draftValues = 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));
        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);
    }

    /**
     * 执行 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<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", "任务当前只读，不能编辑或确认。");
        }
    }

    /**
     * 标准化请求中的 field_values。空请求按空草稿处理。
     */
    private Map<String, Object> normalizeFieldValues(Map<String, Object> rawFieldValues) {
        Map<String, Object> normalized = new LinkedHashMap<>();
        if (rawFieldValues == null) {
            return normalized;
        }
        rawFieldValues.forEach((fieldPath, value) -> normalized.put(fieldPath == null ? "" : fieldPath.trim(), value));
        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 -> matchesMatrixExpression(definition.resultType(), task.resultType()))
                .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) {
        List<String> errors = new ArrayList<>();
        for (ReservationTaskCardFieldDefinition definition : activeDefinitions) {
            Object value = valueForField(aiPayload, editedValues, definition.fieldPath());
            if (validateRequired && isMissingRequired(task, definition, aiPayload, editedValues, value)) {
                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;
    }

    /**
     * 生成字段定义路径集合，用于最终确认时校验全部展示字段。
     */
    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_path 保存，后续 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));
        try {
            return objectMapper.writeValueAsString(payload);
        } catch (Exception exception) {
            throw error(HttpStatus.INTERNAL_SERVER_ERROR, "TASK_PAYLOAD_JSON_INVALID", "任务卡 payload 生成失败。");
        }
    }

    /**
     * 将已保存 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 无效。");
        }
    }

    /**
     * 写入草稿保存或最终确认审计。审计只记录 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;
    }

    /**
     * 按当前已知矩阵展示条件判断字段是否参与本次后端校验。
     */
    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);
        }
        return valueAt(aiPayload, fieldPath);
    }

    /**
     * 判断值是否为空。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(),
                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(),
                valueAt(aiPayload, 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.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 无效。");
        }
    }

    /**
     * 解析人工转换目标类型，只允许 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
    ) {
    }
}
