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

import cn.yunzhixue.ability.center.examsprint.contracts.report.CreateExamSprintReportRequest;
import cn.yunzhixue.ability.center.examsprint.contracts.report.CreateExamSprintReportResponse;
import cn.yunzhixue.ability.center.examsprint.contracts.report.AchievementExamSprintReportPayload;
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportDetailResponse;
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportGenerationStatus;
import cn.yunzhixue.ability.center.examsprint.contracts.report.ExamSprintReportType;
import cn.yunzhixue.ability.center.examsprint.contracts.report.OutlookExamSprintReportPayload;
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.ExamSprintReportRenderer;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportStorage;
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 jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import org.springframework.stereotype.Service;

import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.UUID;

@Service
public class DefaultExamSprintReportApplicationService implements ExamSprintReportApplicationService {

    private static final String REPORT_GENERATION_DISPATCH_FAILED = "report_generation_dispatch_failed";

    private final ExamSprintReportRepository repository;
    private final ExamSprintReportGenerationDispatcher dispatcher;
    private final ExamSprintReportStorage storage;
    private final List<ExamSprintReportRenderer> renderers;
    private final ExamSprintReportProperties properties;
    private final Clock clock;
    private final ObjectMapper objectMapper;
    private final Validator validator;

    public DefaultExamSprintReportApplicationService(
            ExamSprintReportRepository repository,
            ExamSprintReportGenerationDispatcher dispatcher,
            ExamSprintReportStorage storage,
            ExamSprintReportProperties properties,
            Clock clock,
            ObjectMapper objectMapper,
            Validator validator,
            List<ExamSprintReportRenderer> renderers) {
        this.repository = repository;
        this.dispatcher = dispatcher;
        this.storage = storage;
        this.renderers = List.copyOf(renderers);
        this.properties = properties;
        this.clock = clock;
        this.objectMapper = objectMapper;
        this.validator = validator;
    }

    @Override
    public CreateExamSprintReportResponse createReport(CreateExamSprintReportRequest request) {
        validateCreateRequest(request);
        Instant now = clock.instant();
        ExamSprintReport report = ExamSprintReport.pending(
                UUID.randomUUID().toString(),
                request.reportType(),
                request.payload(),
                now,
                now.plus(properties.getRetention()));
        repository.save(report);
        try {
            dispatcher.dispatch(report.reportId());
        } catch (RuntimeException exception) {
            report = repository.save(report.failed(now, dispatchFailureReason(exception)));
        }
        return new CreateExamSprintReportResponse(
                report.reportId(),
                report.reportType(),
                report.generationStatus(),
                report.createdAt(),
                report.expiresAt());
    }

    @Override
    public ExamSprintReportDetailResponse getReport(String reportId) {
        Instant now = clock.instant();
        ExamSprintReport report = requireReport(reportId);
        if (report.isExpiredAt(now) && report.generationStatus() != ExamSprintReportGenerationStatus.EXPIRED) {
            report = repository.save(report.expired(now));
        }

        String downloadUrl = null;
        String previewHtmlUrl = null;
        if (report.generationStatus() == ExamSprintReportGenerationStatus.SUCCESS
                && !report.isExpiredAt(now)
                && report.storageObjectKey() != null) {
            downloadUrl = storage.generateDownloadUrl(report.storageObjectKey(), properties.getDownloadExpiry()).toString();
            previewHtmlUrl = "/api/exam-sprint/reports/" + report.reportId() + "/preview/html";
        }

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

    @Override
    public ReportDownloadContent downloadReport(String reportId) {
        Instant now = clock.instant();
        ExamSprintReport report = requireReport(reportId);
        if (report.isExpiredAt(now)) {
            if (report.generationStatus() != ExamSprintReportGenerationStatus.EXPIRED) {
                repository.save(report.expired(now));
            }
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }
        if (report.generationStatus() != ExamSprintReportGenerationStatus.SUCCESS || report.storageObjectKey() == null) {
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }
        return storage.download(report.storageObjectKey())
                .map(content -> new ReportDownloadContent(content.fileName(), content.bytes(), content.contentType()))
                .orElseThrow(() -> new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE));
    }

    @Override
    public ReportHtmlPreviewContent previewReportHtml(String reportId) {
        Instant now = clock.instant();
        ExamSprintReport report = requireReport(reportId);
        if (report.isExpiredAt(now)) {
            if (report.generationStatus() != ExamSprintReportGenerationStatus.EXPIRED) {
                repository.save(report.expired(now));
            }
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }
        if (report.generationStatus() != ExamSprintReportGenerationStatus.SUCCESS || report.storageObjectKey() == null) {
            throw new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE);
        }
        String html = renderers.stream()
                .filter(renderer -> renderer.supports(report.reportType()))
                .findFirst()
                .map(renderer -> renderer.render(report.payload(), report.updatedAt()))
                .orElseThrow(() -> new BusinessException(ErrorCode.EXAM_SPRINT_REPORT_DOWNLOAD_UNAVAILABLE));
        return new ReportHtmlPreviewContent(html, "text/html;charset=UTF-8");
    }

    public void cleanupExpiredReports() {
        Instant now = clock.instant();
        for (ExamSprintReport report : repository.findExpiredAtOrBefore(now)) {
            try {
                if (report.storageObjectKey() != null) {
                    storage.delete(report.storageObjectKey());
                    repository.save(report.expiredWithStorageCleared(now));
                } else if (report.generationStatus() != ExamSprintReportGenerationStatus.EXPIRED) {
                    repository.save(report.expired(now));
                }
            } catch (RuntimeException ignored) {
                // keep this expired report retriable on a later cleanup run
            }
        }
    }

    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 void validateCreateRequest(CreateExamSprintReportRequest request) {
        if (request.reportType() == ExamSprintReportType.OUTLOOK) {
            validateOutlookPayload(request.payload());
            return;
        }
        if (request.reportType() == ExamSprintReportType.ACHIEVEMENT) {
            validateAchievementPayload(request.payload());
            return;
        }
        throw new BusinessException(ErrorCode.REPORT_TYPE_UNSUPPORTED);
    }

    private void validateOutlookPayload(JsonNode payload) {
        OutlookExamSprintReportPayload reportPayload = readPayload(payload, OutlookExamSprintReportPayload.class);

        Set<ConstraintViolation<OutlookExamSprintReportPayload>> violations = validator.validate(reportPayload);
        if (!violations.isEmpty()) {
            throw new BusinessException(ErrorCode.VALIDATION_ERROR);
        }
    }

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

        Set<ConstraintViolation<AchievementExamSprintReportPayload>> violations = validator.validate(reportPayload);
        if (!violations.isEmpty()) {
            throw new BusinessException(ErrorCode.VALIDATION_ERROR);
        }
    }

    private void validateAchievementPayloadShape(JsonNode payload) {
        requireObjectPayload(payload);
        requireTextualField(payload, "reportTitle");
        requireTextualField(payload, "reportSubtitle");
        requireTextualField(payload, "completionTitle");
        requireTextualField(payload, "completionSubtitle");

        JsonNode summaryMetrics = requireObjectField(payload, "summaryMetrics");
        requireTextualField(summaryMetrics, "vocabularyGrowthText");
        requireTextualField(summaryMetrics, "paperKnownWordsGrowthText");
        requireTextualField(summaryMetrics, "unknownWordHitRateText");
        requireTextualField(summaryMetrics, "learningEfficiencyText");

        validateComparisonShape(requireObjectField(payload, "vocabularyComparison"));
        validateComparisonShape(requireObjectField(payload, "paperKnownWordsComparison"));

        JsonNode examUnknownWordsHitStatus = requireObjectField(payload, "examUnknownWordsHitStatus");
        requireTextualField(examUnknownWordsHitStatus, "unknownWordHitRateText");
        requireTextualField(examUnknownWordsHitStatus, "learningEfficiencyText");
        requireTextualField(examUnknownWordsHitStatus, "unknownWordsBeforeText");
        requireTextualField(examUnknownWordsHitStatus, "unknownWordsAfterText");
        requireTextualField(examUnknownWordsHitStatus, "reducedUnknownWordsText");
        requireTextualArrayField(examUnknownWordsHitStatus, "hitWords");
    }

    private void validateComparisonShape(JsonNode comparison) {
        requireNumericField(comparison, "beforeValue");
        requireNumericField(comparison, "afterValue");
        requireTextualField(comparison, "beforeText");
        requireTextualField(comparison, "afterText");
        requireTextualField(comparison, "growthText");
    }

    private void requireTextualField(JsonNode objectNode, String fieldName) {
        JsonNode field = objectNode.get(fieldName);
        if (field == null || !field.isTextual()) {
            throw new BusinessException(ErrorCode.VALIDATION_ERROR);
        }
    }

    private void requireNumericField(JsonNode objectNode, String fieldName) {
        JsonNode field = objectNode.get(fieldName);
        if (field == null || !field.isNumber()) {
            throw new BusinessException(ErrorCode.VALIDATION_ERROR);
        }
    }

    private JsonNode requireObjectField(JsonNode objectNode, String fieldName) {
        JsonNode field = objectNode.get(fieldName);
        if (field == null || !field.isObject()) {
            throw new BusinessException(ErrorCode.VALIDATION_ERROR);
        }
        return field;
    }

    private void requireTextualArrayField(JsonNode objectNode, String fieldName) {
        JsonNode field = objectNode.get(fieldName);
        if (field == null || !field.isArray()) {
            throw new BusinessException(ErrorCode.VALIDATION_ERROR);
        }
        for (JsonNode element : field) {
            if (!element.isTextual()) {
                throw new BusinessException(ErrorCode.VALIDATION_ERROR);
            }
        }
    }

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

        try {
            T reportPayload = objectMapper.treeToValue(payload, payloadType);
            if (reportPayload == null) {
                throw new BusinessException(ErrorCode.VALIDATION_ERROR);
            }
            return reportPayload;
        } catch (JsonProcessingException | IllegalArgumentException exception) {
            throw new BusinessException(ErrorCode.VALIDATION_ERROR);
        }
    }

    private void requireObjectPayload(JsonNode payload) {
        if (payload == null || payload.isNull() || !payload.isObject()) {
            throw new BusinessException(ErrorCode.VALIDATION_ERROR);
        }
    }
}
