package cn.yunzhixue.ability.center.examsprint.application.report;

import cn.yunzhixue.ability.center.examsprint.contracts.report.AchievementExamSprintReportPayload;
import cn.yunzhixue.ability.center.examsprint.contracts.report.CreateExamSprintReportResponse;
import cn.yunzhixue.ability.center.examsprint.contracts.report.CreateExamSprintReportWithUrlResponse;
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportDetailResponse;
import cn.yunzhixue.ability.center.examsprint.contracts.report.OutlookExamSprintReportPayload;
import cn.yunzhixue.ability.center.examsprint.domain.report.AchievementReportContent;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReport;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRepository;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportContent;
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportGenerationStatus;
import cn.yunzhixue.ability.center.examsprint.domain.report.ReportType;
import cn.yunzhixue.ability.center.examsprint.domain.report.UnmodeledReportContent;
import cn.yunzhixue.ability.center.kernel.BusinessException;
import cn.yunzhixue.ability.center.kernel.ErrorCode;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class DefaultExamSprintReportApplicationService implements ExamSprintReportApplicationService {

    private static final Logger log = LoggerFactory.getLogger(DefaultExamSprintReportApplicationService.class);

    private static final String REPORT_GENERATION_DISPATCH_FAILED = "report_generation_dispatch_failed";
    private static final BigDecimal ACHIEVEMENT_MAX_NUMERIC_ABS = new BigDecimal("1000000000");
    private static final int ACHIEVEMENT_MAX_NUMERIC_PRECISION = 12;
    private static final int ACHIEVEMENT_MAX_NUMERIC_SCALE = 6;
    private static final String OUTLOOK_REPORT_NAME = "展望报告";
    private static final String ACHIEVEMENT_REPORT_NAME = "成果报告";

    private final ExamSprintReportRepository repository;
    private final ExamSprintReportGenerationDispatcher dispatcher;
    private final ExamSprintReportGenerationPipeline pipeline;
    private final ExamSprintReportStorage storage;
    private final ExamSprintReportProperties properties;
    private final Clock clock;
    private final ObjectMapper objectMapper;
    private final Validator validator;

    public DefaultExamSprintReportApplicationService(
            ExamSprintReportRepository repository,
            ExamSprintReportGenerationDispatcher dispatcher,
            ExamSprintReportGenerationPipeline pipeline,
            ExamSprintReportStorage storage,
            ExamSprintReportProperties properties,
            Clock clock,
            ObjectMapper objectMapper,
            Validator validator) {
        this.repository = repository;
        this.dispatcher = dispatcher;
        this.pipeline = pipeline;
        this.storage = storage;
        this.properties = properties;
        this.clock = clock;
        this.objectMapper = objectMapper;
        this.validator = validator;
    }

    @Override
    public CreateExamSprintReportResponse createOutlookReport(JsonNode payload) {
        validateOutlookPayload(payload);
        JsonNode normalizedPayload = normalizeOutlookPayload(payload);
        return submitReportGeneration(ReportType.OUTLOOK, new UnmodeledReportContent(ReportType.OUTLOOK, normalizedPayload));
    }

    @Override
    public CreateExamSprintReportResponse createAchievementReport(JsonNode payload) {
        AchievementReportContent content = validateAchievementPayload(payload);
        return submitReportGeneration(ReportType.ACHIEVEMENT, content);
    }

    @Override
    public CreateExamSprintReportWithUrlResponse createOutlookReportSync(JsonNode payload) {
        validateOutlookPayload(payload);
        JsonNode normalizedPayload = normalizeOutlookPayload(payload);
        return submitReportGenerationSync(ReportType.OUTLOOK, new UnmodeledReportContent(ReportType.OUTLOOK, normalizedPayload));
    }

    @Override
    public CreateExamSprintReportWithUrlResponse createAchievementReportSync(JsonNode payload) {
        AchievementReportContent content = validateAchievementPayload(payload);
        return submitReportGenerationSync(ReportType.ACHIEVEMENT, content);
    }

    private CreateExamSprintReportResponse submitReportGeneration(ReportType reportType, ReportContent content) {
        Instant now = clock.instant();
        ExamSprintReport report = ExamSprintReport.pending(
                UUID.randomUUID().toString(),
                reportType,
                content,
                now,
                now.plus(properties.getRetention()));
        repository.save(report);
        log.info(
                "exam_sprint_report_submitted reportId={} reportType={} generationStatus={} expiresAt={} mode=async",
                report.reportId(),
                report.reportType(),
                report.generationStatus(),
                report.expiresAt());
        try {
            dispatcher.dispatch(report.reportId());
            log.info(
                    "exam_sprint_report_dispatched reportId={} reportType={} mode=async",
                    report.reportId(),
                    report.reportType());
        } catch (RuntimeException exception) {
            report = repository.save(report.failed(now, dispatchFailureReason(exception)));
            log.warn(
                    "exam_sprint_report_dispatch_failed reportId={} reportType={} failureReason={} exceptionType={}",
                    report.reportId(),
                    report.reportType(),
                    report.failureReason(),
                    exceptionType(exception));
        }
        return new CreateExamSprintReportResponse(
                report.reportId(),
                ExamSprintReportContractMapper.toContractReportType(report.reportType()),
                ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
                report.createdAt(),
                report.expiresAt());
    }

    private CreateExamSprintReportWithUrlResponse submitReportGenerationSync(
            ReportType reportType,
            ReportContent content) {
        long startedNanos = System.nanoTime();
        Instant now = clock.instant();
        ExamSprintReport report = ExamSprintReport.pending(
                UUID.randomUUID().toString(),
                reportType,
                content,
                now,
                now.plus(properties.getRetention()));
        repository.save(report);
        log.info(
                "exam_sprint_report_sync_generation_started reportId={} reportType={} generationStatus={} expiresAt={}",
                report.reportId(),
                report.reportType(),
                report.generationStatus(),
                report.expiresAt());

        Optional<ExamSprintReport> generatedReportOption = pipeline.generate(report.reportId());
        if (generatedReportOption.isEmpty()) {
            log.warn(
                    "exam_sprint_report_sync_generation_unavailable reportId={} reportType={} reason=pipeline_empty durationMs={}",
                    report.reportId(),
                    report.reportType(),
                    elapsedMillis(startedNanos));
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }

        ExamSprintReport generatedReport = generatedReportOption.orElseThrow();
        if (generatedReport.generationStatus() != ReportGenerationStatus.SUCCESS
                || generatedReport.storageObjectKey() == null) {
            log.warn(
                    "exam_sprint_report_sync_generation_unavailable reportId={} reportType={} generationStatus={} storageObjectKeyPresent={} durationMs={}",
                    generatedReport.reportId(),
                    generatedReport.reportType(),
                    generatedReport.generationStatus(),
                    generatedReport.storageObjectKey() != null,
                    elapsedMillis(startedNanos));
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }

        String downloadUrl;
        try {
            downloadUrl = storage.generateDownloadUrl(
                    generatedReport.storageObjectKey(),
                    properties.getDownloadExpiry()).toString();
        } catch (RuntimeException exception) {
            log.warn(
                    "exam_sprint_report_sync_generation_unavailable reportId={} reportType={} generationStatus={} reason=download_url_generation_failed exceptionType={} durationMs={}",
                    generatedReport.reportId(),
                    generatedReport.reportType(),
                    generatedReport.generationStatus(),
                    exceptionType(exception),
                    elapsedMillis(startedNanos));
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }
        log.info(
                "exam_sprint_report_sync_generation_succeeded reportId={} reportType={} generationStatus={} durationMs={} storageObjectKey={}",
                generatedReport.reportId(),
                generatedReport.reportType(),
                generatedReport.generationStatus(),
                elapsedMillis(startedNanos),
                generatedReport.storageObjectKey());
        return new CreateExamSprintReportWithUrlResponse(
                generatedReport.reportId(),
                ExamSprintReportContractMapper.toContractReportType(generatedReport.reportType()),
                ExamSprintReportContractMapper.toContractStatus(generatedReport.generationStatus()),
                generatedReport.createdAt(),
                generatedReport.updatedAt(),
                generatedReport.expiresAt(),
                downloadUrl);
    }

    @Override
    public ExamSprintReportDetailResponse getReport(String reportId) {
        Instant now = clock.instant();
        ExamSprintReport report = requireReport(reportId);
        if (report.isExpiredAt(now) && report.generationStatus() != ReportGenerationStatus.EXPIRED) {
            report = repository.save(report.expired(now));
            log.info(
                    "exam_sprint_report_marked_expired_on_query reportId={} reportType={} generationStatus={} expiresAt={}",
                    report.reportId(),
                    report.reportType(),
                    report.generationStatus(),
                    report.expiresAt());
        }

        String downloadUrl = null;
        if (report.generationStatus() == ReportGenerationStatus.SUCCESS
                && !report.isExpiredAt(now)
                && report.storageObjectKey() != null) {
            try {
                downloadUrl = storage.generateDownloadUrl(report.storageObjectKey(), properties.getDownloadExpiry()).toString();
            } catch (RuntimeException exception) {
                log.warn(
                        "exam_sprint_report_download_url_generation_failed reportId={} reportType={} generationStatus={} storageObjectKey={} exceptionType={}",
                        report.reportId(),
                        report.reportType(),
                        report.generationStatus(),
                        report.storageObjectKey(),
                        exceptionType(exception));
            }
        }

        boolean downloadUrlIncluded = downloadUrl != null;
        log.info(
                "exam_sprint_report_query_completed reportId={} reportType={} generationStatus={} downloadUrlIncluded={} storageObjectKeyPresent={}",
                report.reportId(),
                report.reportType(),
                report.generationStatus(),
                downloadUrlIncluded,
                report.storageObjectKey() != null);

        return new ExamSprintReportDetailResponse(
                report.reportId(),
                ExamSprintReportContractMapper.toContractReportType(report.reportType()),
                ExamSprintReportContractMapper.toContractStatus(report.generationStatus()),
                report.createdAt(),
                report.updatedAt(),
                report.expiresAt(),
                downloadUrl,
                report.failureReason());
    }

    @Override
    public ReportDownloadContent downloadReport(String reportId) {
        Instant now = clock.instant();
        ExamSprintReport report = requireReport(reportId);
        log.info(
                "exam_sprint_report_download_started reportId={} reportType={} generationStatus={}",
                report.reportId(),
                report.reportType(),
                report.generationStatus());
        if (report.isExpiredAt(now)) {
            if (report.generationStatus() != ReportGenerationStatus.EXPIRED) {
                report = repository.save(report.expired(now));
            }
            log.warn(
                    "exam_sprint_report_download_unavailable reportId={} reportType={} generationStatus={} reason=expired",
                    report.reportId(),
                    report.reportType(),
                    report.generationStatus());
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }
        if (report.generationStatus() != ReportGenerationStatus.SUCCESS) {
            log.warn(
                    "exam_sprint_report_download_unavailable reportId={} reportType={} generationStatus={} reason=not_success",
                    report.reportId(),
                    report.reportType(),
                    report.generationStatus());
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }
        if (report.storageObjectKey() == null) {
            log.warn(
                    "exam_sprint_report_download_unavailable reportId={} reportType={} generationStatus={} reason=missing_storage_key",
                    report.reportId(),
                    report.reportType(),
                    report.generationStatus());
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }

        Optional<ExamSprintReportStorage.StoredExamSprintReportContent> content;
        try {
            content = storage.download(report.storageObjectKey());
        } catch (RuntimeException exception) {
            log.warn(
                    "exam_sprint_report_download_unavailable reportId={} reportType={} generationStatus={} reason=storage_download_failed storageObjectKey={} exceptionType={}",
                    report.reportId(),
                    report.reportType(),
                    report.generationStatus(),
                    report.storageObjectKey(),
                    exceptionType(exception));
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }
        if (content.isEmpty()) {
            log.warn(
                    "exam_sprint_report_download_missing_storage_content reportId={} reportType={} storageObjectKey={}",
                    report.reportId(),
                    report.reportType(),
                    report.storageObjectKey());
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }

        ReportDownloadContent downloadContent = content.map(storedContent -> new ReportDownloadContent(
                        storedContent.fileName(),
                        storedContent.bytes(),
                        storedContent.contentType()))
                .orElseThrow();
        log.info(
                "exam_sprint_report_download_succeeded reportId={} reportType={} storageObjectKey={} fileName={} byteLength={}",
                report.reportId(),
                report.reportType(),
                report.storageObjectKey(),
                downloadContent.fileName(),
                downloadContent.bytes().length);
        return downloadContent;
    }

    public void cleanupExpiredReports() {
        Instant now = clock.instant();
        List<ExamSprintReport> expiredReports = repository.findExpiredAtOrBefore(now);
        int storageClearedCount = 0;
        int markedExpiredCount = 0;
        int failedCount = 0;
        for (ExamSprintReport report : expiredReports) {
            try {
                if (report.storageObjectKey() != null) {
                    storage.delete(report.storageObjectKey());
                    repository.save(report.expiredWithStorageCleared(now));
                    storageClearedCount++;
                } else if (report.generationStatus() != ReportGenerationStatus.EXPIRED) {
                    repository.save(report.expired(now));
                    markedExpiredCount++;
                }
            } catch (RuntimeException exception) {
                failedCount++;
                log.warn(
                        "exam_sprint_report_cleanup_item_failed reportId={} reportType={} generationStatus={} storageObjectKey={} exceptionType={}",
                        report.reportId(),
                        report.reportType(),
                        report.generationStatus(),
                        report.storageObjectKey(),
                        exceptionType(exception));
                // keep this expired report retriable on a later cleanup run
            }
        }
        log.info(
                "exam_sprint_report_cleanup_completed scannedCount={} storageClearedCount={} markedExpiredCount={} failedCount={}",
                expiredReports.size(),
                storageClearedCount,
                markedExpiredCount,
                failedCount);
    }

    private ExamSprintReport requireReport(String reportId) {
        return repository.findById(reportId)
                .orElseThrow(() -> new BusinessException(ErrorCode.REPORT_NOT_FOUND));
    }

    private String dispatchFailureReason(RuntimeException exception) {
        return REPORT_GENERATION_DISPATCH_FAILED;
    }

    private String exceptionType(RuntimeException exception) {
        return exception.getClass().getSimpleName();
    }

    private long elapsedMillis(long startedNanos) {
        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedNanos);
    }

    private void validateOutlookPayload(JsonNode payload) {
        validateOutlookStudentNameFields(payload);
        validateOutlookPayloadShape(payload);
        OutlookExamSprintReportPayload reportPayload = readPayload(
                outlookPayloadForValidation(payload),
                OutlookExamSprintReportPayload.class,
                OUTLOOK_REPORT_NAME);
        validatePayload(reportPayload, OUTLOOK_REPORT_NAME, this::toOutlookFieldPath);
    }

    private void validateOutlookStudentNameFields(JsonNode payload) {
        requireObjectPayload(payload, OUTLOOK_REPORT_NAME);
        if (resolveOutlookStudentName(payload).isPresent()) {
            return;
        }
        if (hasNonNullNonTextualField(payload, "StudentName")) {
            throw validationException(OUTLOOK_REPORT_NAME, "字段 StudentName 必须为非空字符串");
        }
        if (hasNonNullNonTextualField(payload, "studentName")) {
            throw validationException(OUTLOOK_REPORT_NAME, "字段 studentName 必须为非空字符串");
        }
    }

    private void validateOutlookPayloadShape(JsonNode payload) {
        requireObjectPayload(payload, OUTLOOK_REPORT_NAME);
        requireOutlookIntegralField(payload, "StudentStage");
        requireOutlookTextualField(payload, "StageName");
        requireOutlookIntegralField(payload, "StageVocabulary");
        requireOutlookTextualField(payload, "StageExaminName");
        requireOutlookIntegralField(payload, "StageImportant");
        requireOutlookArrayField(payload, "StudentWordsLatest");
        requireOutlookIntegralField(payload, "MastedWordCount");
        requireOutlookIntegralField(payload, "UnMastedWordCount");
        requireOutlookIntegralField(payload, "ExamineStrangeWordCount");
        requireOutlookArrayField(payload, "TestPaperWordIdArray");
        requireOutlookTextualField(payload, "TestPaperTitle");
        requireOutlookArrayField(payload, "TestPaperUnMasterWords");
        requireOutlookArrayField(payload, "TestPaperMastedWords");
        requireOutlookIntegralField(payload, "TestPaperMastedWordCount");
        requireOutlookIntegralField(payload, "TestPaperWordCount");
        requireOutlookBooleanField(payload, "Complex");
    }

    private void requireOutlookTextualField(JsonNode objectNode, String fieldPath) {
        JsonNode field = objectNode.get(leafFieldName(fieldPath));
        if (field == null || !field.isTextual()) {
            throw validationException(OUTLOOK_REPORT_NAME, "字段 " + fieldPath + " 必须为字符串");
        }
    }

    private void requireOutlookIntegralField(JsonNode objectNode, String fieldPath) {
        JsonNode field = objectNode.get(leafFieldName(fieldPath));
        if (field == null || !field.isIntegralNumber()) {
            throw validationException(OUTLOOK_REPORT_NAME, "字段 " + fieldPath + " 必须为数字");
        }
    }

    private void requireOutlookArrayField(JsonNode objectNode, String fieldPath) {
        JsonNode field = objectNode.get(leafFieldName(fieldPath));
        if (field == null || !field.isArray()) {
            throw validationException(OUTLOOK_REPORT_NAME, "字段 " + fieldPath + " 必须为数组");
        }
    }

    private void requireOutlookBooleanField(JsonNode objectNode, String fieldPath) {
        JsonNode field = objectNode.get(leafFieldName(fieldPath));
        if (field == null || !field.isBoolean()) {
            throw validationException(OUTLOOK_REPORT_NAME, "字段 " + fieldPath + " 必须为布尔值");
        }
    }

    private JsonNode normalizeOutlookPayload(JsonNode payload) {
        requireObjectPayload(payload, OUTLOOK_REPORT_NAME);
        ObjectNode normalizedPayload = ((ObjectNode) payload).deepCopy();
        resolveOutlookStudentName(normalizedPayload).ifPresent(studentName -> {
            normalizedPayload.put("StudentName", studentName);
            normalizedPayload.put("studentName", studentName);
        });
        return normalizedPayload;
    }

    private Optional<String> resolveOutlookStudentName(JsonNode payload) {
        Optional<String> canonicalStudentName = textualNonBlankValue(payload, "StudentName");
        return canonicalStudentName.isPresent()
                ? canonicalStudentName
                : textualNonBlankValue(payload, "studentName");
    }

    private Optional<String> textualNonBlankValue(JsonNode payload, String fieldName) {
        JsonNode field = payload.get(fieldName);
        if (field == null || !field.isTextual()) {
            return Optional.empty();
        }
        String trimmedValue = field.asText().trim();
        return trimmedValue.isBlank() ? Optional.empty() : Optional.of(trimmedValue);
    }

    private boolean hasNonNullNonTextualField(JsonNode payload, String fieldName) {
        JsonNode field = payload.get(fieldName);
        return field != null && !field.isNull() && !field.isTextual();
    }

    private JsonNode outlookPayloadForValidation(JsonNode payload) {
        requireObjectPayload(payload, OUTLOOK_REPORT_NAME);
        if (!payload.has("StudentName") || !payload.has("studentName")) {
            return payload;
        }

        ObjectNode validationPayload = ((ObjectNode) payload).deepCopy();
        resolveOutlookStudentName(validationPayload)
                .ifPresent(studentName -> validationPayload.put("StudentName", studentName));
        validationPayload.remove("studentName");
        return validationPayload;
    }

    private <T> void validatePayload(T reportPayload, String reportName, java.util.function.Function<String, String> fieldPathMapper) {
        Set<ConstraintViolation<T>> violations = validator.validate(reportPayload);
        if (!violations.isEmpty()) {
            String detail = violations.stream()
                    .sorted(Comparator.comparing(violation -> violation.getPropertyPath().toString()))
                    .map(violation -> "字段 " + fieldPathMapper.apply(violation.getPropertyPath().toString()) + " "
                            + violationDescription(violation))
                    .findFirst()
                    .orElse("请求参数不满足约束");
            throw validationException(reportName, detail);
        }
    }

    private AchievementReportContent validateAchievementPayload(JsonNode payload) {
        validateAchievementPayloadShape(payload);
        AchievementExamSprintReportPayload reportPayload = readPayload(
                payload,
                AchievementExamSprintReportPayload.class,
                ACHIEVEMENT_REPORT_NAME);

        Set<ConstraintViolation<AchievementExamSprintReportPayload>> violations = validator.validate(reportPayload);
        if (!violations.isEmpty()) {
            String detail = violations.stream()
                    .sorted(Comparator.comparing(violation -> violation.getPropertyPath().toString()))
                    .map(violation -> "字段 " + violation.getPropertyPath() + " " + violationDescription(violation))
                    .findFirst()
                    .orElse("请求参数不满足约束");
            throw validationException(ACHIEVEMENT_REPORT_NAME, detail);
        }
        return AchievementReportContentMapper.toDomainContent(reportPayload);
    }

    private void validateAchievementPayloadShape(JsonNode payload) {
        requireObjectPayload(payload, ACHIEVEMENT_REPORT_NAME);
        requireTextualField(payload, "StudentName");
        requireTextualField(payload, "StageName");
        requireTextualField(payload, "TestPaperTitle");

        requireNonNegativeNumericField(payload, "StudentStage");
        requireNonNegativeNumericField(payload, "StageVocabulary");
        requireNonNegativeNumericField(payload, "StudentVocabulary");
        requireNonNegativeNumericField(payload, "StudentVocabularyBefore");
        requireNonNegativeNumericField(payload, "StudentUnMastedWordCount");
        requireNumericField(payload, "StudentImproveWordCount", true);
        requireNonNegativeNumericField(payload, "TestPaperWordCount");
        requireNonNegativeNumericField(payload, "TestPaperBeforUnMastery");
        requireNonNegativeNumericField(payload, "TestPaperBeforMastery");
        requireNonNegativeNumericField(payload, "TestPaperLatestMastery");
        requireNonNegativeNumericField(payload, "TestPaperAfterUnMastery");
        requireNumericField(payload, "TestPaperImprovedWordCount", true);
        requireNumericField(payload, "TestPaperImproveRate", true);
        requireNonNegativeNumericField(payload, "PaperMasteryHitRate");
        requireNonNegativeNumericField(payload, "ImproveStudyEfficiency");
        requireNonNegativeNumericField(payload, "StudentInitialVocabMastery");
        requireNonNegativeNumericField(payload, "StudentCurrentVocabMastery");
        requireNumericField(payload, "StudentVocabMasteryImprovement", true);
        requireNonNegativeNumericField(payload, "TestPaperBeforMasteryRate");
        requireNonNegativeNumericField(payload, "TestPaperLatestMasteryRate");

        requireImprovedWordsArrayField(payload, "TestPaperImprovedWords");
        requireOptionalBooleanField(payload, "ShouldDisplaySigningGuarantee");
        requireOptionalTextualField(payload, "SigningGuarantee");
    }

    private void requireTextualField(JsonNode objectNode, String fieldPath) {
        JsonNode field = objectNode.get(leafFieldName(fieldPath));
        if (field == null || !field.isTextual()) {
            throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldPath + " 必须为字符串");
        }
    }

    private void requireNonNegativeNumericField(JsonNode objectNode, String fieldName) {
        requireNumericField(objectNode, fieldName, false);
    }

    private void requireNumericField(JsonNode objectNode, String fieldPath, boolean allowNegative) {
        JsonNode field = objectNode.get(leafFieldName(fieldPath));
        if (field == null || !field.isNumber()) {
            throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldPath + " 必须为数字");
        }
        BigDecimal value;
        try {
            value = field.decimalValue();
        } catch (RuntimeException exception) {
            throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldPath + " 必须为合法数字");
        }
        if ((!allowNegative && value.signum() < 0)
                || value.precision() > ACHIEVEMENT_MAX_NUMERIC_PRECISION
                || value.scale() > ACHIEVEMENT_MAX_NUMERIC_SCALE
                || value.abs().compareTo(ACHIEVEMENT_MAX_NUMERIC_ABS) > 0) {
            throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldPath + " 数值超出允许范围");
        }
    }

    private JsonNode requireObjectField(JsonNode objectNode, String fieldName) {
        JsonNode field = objectNode.get(fieldName);
        if (field == null || !field.isObject()) {
            throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldName + " 必须为对象");
        }
        return field;
    }

    private void requireOptionalBooleanField(JsonNode objectNode, String fieldName) {
        JsonNode field = objectNode.get(fieldName);
        if (field != null && !field.isNull() && !field.isBoolean()) {
            throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldName + " 必须为布尔值");
        }
    }

    private void requireOptionalTextualField(JsonNode objectNode, String fieldName) {
        JsonNode field = objectNode.get(fieldName);
        if (field != null && !field.isNull() && !field.isTextual()) {
            throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldName + " 必须为字符串");
        }
    }

    private void requireImprovedWordsArrayField(JsonNode objectNode, String fieldName) {
        JsonNode field = objectNode.get(fieldName);
        if (field == null || !field.isArray()) {
            throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldName + " 必须为数组");
        }
        int index = 0;
        for (JsonNode element : field) {
            if (element.isTextual()) {
                index++;
                continue;
            }
            if (!element.isObject() || !element.path("WordSpell").isTextual()) {
                throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldName + "[" + index + "] 必须为字符串或包含 WordSpell 的对象");
            }
            index++;
        }
    }

    private void requireTextualArrayField(JsonNode objectNode, String fieldPath) {
        JsonNode field = objectNode.get(leafFieldName(fieldPath));
        if (field == null || !field.isArray()) {
            throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldPath + " 必须为字符串数组");
        }
        int index = 0;
        for (JsonNode element : field) {
            if (!element.isTextual()) {
                throw validationException(ACHIEVEMENT_REPORT_NAME, "字段 " + fieldPath + "[" + index + "] 必须为字符串");
            }
            index++;
        }
    }

    private <T> T readPayload(JsonNode payload, Class<T> payloadType, String reportName) {
        requireObjectPayload(payload, reportName);

        try {
            T reportPayload = objectMapper.treeToValue(payload, payloadType);
            if (reportPayload == null) {
                throw validationException(reportName, "payload 不能为空");
            }
            return reportPayload;
        } catch (JsonProcessingException | IllegalArgumentException exception) {
            throw validationException(reportName, "payload 无法解析为报告请求结构，请检查字段类型和结构");
        }
    }

    private void requireObjectPayload(JsonNode payload, String reportName) {
        if (payload == null || payload.isNull() || !payload.isObject()) {
            throw validationException(reportName, "payload 必须为 JSON 对象");
        }
    }

    private String leafFieldName(String fieldPath) {
        int separatorIndex = fieldPath.lastIndexOf('.');
        return separatorIndex < 0 ? fieldPath : fieldPath.substring(separatorIndex + 1);
    }

    private BusinessException validationException(String reportName, String detail) {
        return new BusinessException(ErrorCode.VALIDATION_ERROR, reportName + "参数校验失败：" + detail);
    }

    private String violationDescription(ConstraintViolation<?> violation) {
        Class<?> annotationType = violation.getConstraintDescriptor().getAnnotation().annotationType();
        if (annotationType == NotBlank.class) {
            return "必须为非空字符串";
        }
        if (annotationType == NotNull.class) {
            return "不能为空";
        }
        if (annotationType == NotEmpty.class) {
            return "不能为空数组或集合";
        }
        if (annotationType == Min.class) {
            return "必须大于等于 " + violation.getConstraintDescriptor().getAttributes().get("value");
        }
        if (annotationType == DecimalMin.class) {
            return "必须大于等于 " + violation.getConstraintDescriptor().getAttributes().get("value");
        }
        if (annotationType == DecimalMax.class) {
            return "必须小于等于 " + violation.getConstraintDescriptor().getAttributes().get("value");
        }
        return "不满足约束";
    }

    private String toOutlookFieldPath(String propertyPath) {
        StringBuilder mappedPath = new StringBuilder();
        for (String segment : propertyPath.split("\\.")) {
            if (segment.startsWith("<") && segment.endsWith(">")) {
                continue;
            }
            if (!mappedPath.isEmpty()) {
                mappedPath.append('.');
            }
            mappedPath.append(toOutlookFieldPathSegment(segment));
        }
        return mappedPath.toString();
    }

    private String toOutlookFieldPathSegment(String propertyPathSegment) {
        int indexedPathStart = propertyPathSegment.indexOf('[');
        String propertyName = indexedPathStart < 0
                ? propertyPathSegment
                : propertyPathSegment.substring(0, indexedPathStart);
        String indexSuffix = indexedPathStart < 0 ? "" : propertyPathSegment.substring(indexedPathStart);
        return switch (propertyName) {
            case "studentName" -> "StudentName";
            case "studentStage" -> "StudentStage";
            case "stageName" -> "StageName";
            case "stageVocabulary" -> "StageVocabulary";
            case "stageExaminName" -> "StageExaminName";
            case "stageImportant" -> "StageImportant";
            case "studentWordsLatest" -> "StudentWordsLatest";
            case "mastedWordCount" -> "MastedWordCount";
            case "unMastedWordCount" -> "UnMastedWordCount";
            case "examineStrangeWordCount" -> "ExamineStrangeWordCount";
            case "testPaperWordIdArray" -> "TestPaperWordIdArray";
            case "testPaperTitle" -> "TestPaperTitle";
            case "testPaperUnMasterWords" -> "TestPaperUnMasterWords";
            case "testPaperMastedWords" -> "TestPaperMastedWords";
            case "testPaperMastedWordCount" -> "TestPaperMastedWordCount";
            case "testPaperWordCount" -> "TestPaperWordCount";
            case "complex" -> "Complex";
            case "wordId" -> "WordId";
            case "meanId" -> "MeanId";
            case "wordSpell" -> "WordSpell";
            case "wordFrequency" -> "WordFrequency";
            case "mastery" -> "Mastery";
            case "reviewTimes" -> "ReviewTimes";
            case "reliability" -> "Reliability";
            case "createTime" -> "CreateTime";
            default -> propertyName;
        } + indexSuffix;
    }
}
