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

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.ExamSprintReportPdfGenerator;
import cn.yunzhixue.ability.center.examsprint.domain.report.ExamSprintReportRenderer;
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 com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Service
public class ExamSprintReportGenerationPipeline {

    private static final Logger log = LoggerFactory.getLogger(ExamSprintReportGenerationPipeline.class);
    private static final ZoneId FILE_NAME_ZONE = ZoneId.of("Asia/Shanghai");
    private static final DateTimeFormatter FILE_NAME_TIME_FORMATTER =
            DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(FILE_NAME_ZONE);

    private final ExamSprintReportRepository repository;
    private final List<ExamSprintReportRenderer> renderers;
    private final ExamSprintReportPdfGenerator pdfGenerator;
    private final ExamSprintReportStorage storage;
    private final Clock clock;

    public ExamSprintReportGenerationPipeline(
            ExamSprintReportRepository repository,
            List<ExamSprintReportRenderer> renderers,
            ExamSprintReportPdfGenerator pdfGenerator,
            ExamSprintReportStorage storage,
            Clock clock) {
        this.repository = repository;
        this.renderers = List.copyOf(renderers);
        this.pdfGenerator = pdfGenerator;
        this.storage = storage;
        this.clock = clock;
    }

    public Optional<ExamSprintReport> generate(String reportId) {
        Instant startedAt = clock.instant();
        long startedNanos = System.nanoTime();
        ExamSprintReport report = repository.findById(reportId).orElse(null);
        if (report == null) {
            log.info("exam_sprint_report_generation_skipped reportId={} reason=not_found", reportId);
            return Optional.empty();
        }

        if (report.generationStatus() != ReportGenerationStatus.PENDING) {
            log.info(
                    "exam_sprint_report_generation_skipped reportId={} reportType={} generationStatus={} reason=not_pending",
                    reportId,
                    report.reportType(),
                    report.generationStatus());
            return Optional.empty();
        }

        if (report.isExpiredAt(startedAt)) {
            log.info(
                    "exam_sprint_report_generation_skipped reportId={} reportType={} generationStatus={} reason=expired expiresAt={}",
                    reportId,
                    report.reportType(),
                    report.generationStatus(),
                    report.expiresAt());
            return Optional.empty();
        }

        log.info(
                "exam_sprint_report_generation_started reportId={} reportType={} generationStatus={} startedAt={}",
                reportId,
                report.reportType(),
                report.generationStatus(),
                startedAt);

        ExamSprintReport processingReport = repository.save(report.processing(startedAt));
        log.info(
                "exam_sprint_report_generation_processing reportId={} reportType={} generationStatus={}",
                processingReport.reportId(),
                processingReport.reportType(),
                processingReport.generationStatus());

        String stage = "renderer_selection";
        try {
            ExamSprintReportRenderer renderer = rendererFor(processingReport);

            stage = "render_html";
            long renderStartedNanos = System.nanoTime();
            String html = renderer.render(processingReport.content(), startedAt);
            log.info(
                    "exam_sprint_report_generation_stage_completed reportId={} reportType={} stage=render_html durationMs={} htmlLength={}",
                    processingReport.reportId(),
                    processingReport.reportType(),
                    elapsedMillis(renderStartedNanos),
                    html.length());

            stage = "pdf_generation";
            long pdfStartedNanos = System.nanoTime();
            byte[] pdfBytes = pdfGenerator.generate(html);
            log.info(
                    "exam_sprint_report_generation_stage_completed reportId={} reportType={} stage=pdf_generation durationMs={} pdfByteLength={}",
                    processingReport.reportId(),
                    processingReport.reportType(),
                    elapsedMillis(pdfStartedNanos),
                    pdfBytes.length);

            stage = "storage_upload";
            long uploadStartedNanos = System.nanoTime();
            String fileName = fileNameOf(processingReport);
            ExamSprintReportStorage.StoredExamSprintReportFile storedFile = storage.upload(
                    processingReport.reportId(),
                    processingReport.reportType(),
                    fileName,
                    pdfBytes,
                    processingReport.expiresAt());
            log.info(
                    "exam_sprint_report_generation_stage_completed reportId={} reportType={} stage=storage_upload durationMs={} storageObjectKey={} fileName={}",
                    processingReport.reportId(),
                    processingReport.reportType(),
                    elapsedMillis(uploadStartedNanos),
                    storedFile.storageObjectKey(),
                    storedFile.fileName());

            stage = "final_success_save";
            Optional<ExamSprintReport> currentReport = repository.findById(reportId);
            if (currentReport.isEmpty()) {
                log.info(
                        "exam_sprint_report_generation_skipped reportId={} reportType={} stage=success_save reason=not_found_before_success_save",
                        reportId,
                        processingReport.reportType());
                return Optional.empty();
            }

            ExamSprintReport successReport = repository.save(currentReport.orElseThrow()
                    .success(clock.instant(), storedFile.storageObjectKey(), storedFile.fileName()));
            log.info(
                    "exam_sprint_report_generation_succeeded reportId={} reportType={} generationStatus={} durationMs={} storageObjectKey={} fileName={}",
                    successReport.reportId(),
                    successReport.reportType(),
                    successReport.generationStatus(),
                    elapsedMillis(startedNanos),
                    successReport.storageObjectKey(),
                    successReport.fileName());
            return Optional.of(successReport);
        } catch (Exception exception) {
            log.error(
                    "exam_sprint_report_generation_failed reportId={} reportType={} stage={} failureReason={} exceptionType={} durationMs={}",
                    reportId,
                    processingReport.reportType(),
                    stage,
                    safeFailureReasonForLog(exception),
                    exception.getClass().getSimpleName(),
                    elapsedMillis(startedNanos));
            return repository.findById(reportId)
                    .map(current -> repository.save(current.failed(clock.instant(), failureReasonOf(exception))));
        }
    }

    private ExamSprintReportRenderer rendererFor(ExamSprintReport report) {
        return renderers.stream()
                .filter(renderer -> renderer.supports(report.reportType()))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("No renderer for report type " + report.reportType()));
    }

    private String fileNameOf(ExamSprintReport report) {
        return studentNameOrReportId(report)
                + "-"
                + reportTitle(report.reportType())
                + "-"
                + FILE_NAME_TIME_FORMATTER.format(clock.instant())
                + ".pdf";
    }

    private String studentNameOrReportId(ExamSprintReport report) {
        String studentName = studentNameFromContent(report.content());
        String trimmedStudentName = studentName == null ? null : studentName.trim();
        return trimmedStudentName == null || trimmedStudentName.isBlank() ? report.reportId() : trimmedStudentName;
    }

    private String studentNameFromContent(ReportContent content) {
        if (content instanceof UnmodeledReportContent unmodeledReportContent
                && unmodeledReportContent.source() instanceof JsonNode payload) {
            JsonNode canonicalStudentName = payload.get("StudentName");
            if (canonicalStudentName != null
                    && canonicalStudentName.isTextual()
                    && !canonicalStudentName.asText().trim().isBlank()) {
                return canonicalStudentName.asText();
            }
            JsonNode studentName = payload.get("studentName");
            return studentName != null && studentName.isTextual() ? studentName.asText() : null;
        }
        if (content instanceof AchievementReportContent achievementReportContent) {
            return achievementReportContent.studentName();
        }
        return null;
    }

    private String reportTitle(ReportType reportType) {
        return switch (reportType) {
            case OUTLOOK -> "临考词汇突击潜力展望报告";
            case ACHIEVEMENT -> "临考词汇突击成果报告";
        };
    }

    private String failureReasonOf(Exception exception) {
        String message = exception.getMessage();
        return message == null || message.isBlank() ? exception.getClass().getSimpleName() : message;
    }

    private String safeFailureReasonForLog(Exception exception) {
        return exception.getClass().getSimpleName();
    }

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