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

import cn.yunzhixue.ability.center.examsprint.contracts.report.AchievementExamSprintReportPayload;
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.ReportContent;
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 com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;

import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

public class ExamSprintReportWarmupRunner {

    static final String OUTLOOK_WARMUP_RESOURCE = "warmup/outlook-exam-sprint-report.json";
    static final String ACHIEVEMENT_WARMUP_RESOURCE = "warmup/achievement-exam-sprint-report.json";

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

    private final List<ExamSprintReportRenderer> renderers;
    private final ExamSprintReportPdfGenerator pdfGenerator;
    private final ObjectMapper objectMapper;
    private final Clock clock;
    private final Executor executor;

    public ExamSprintReportWarmupRunner(
            List<ExamSprintReportRenderer> renderers,
            ExamSprintReportPdfGenerator pdfGenerator,
            ObjectMapper objectMapper,
            Clock clock,
            @Qualifier("examSprintReportExecutor") Executor executor) {
        this.renderers = List.copyOf(renderers);
        this.pdfGenerator = Objects.requireNonNull(pdfGenerator, "pdfGenerator");
        this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper");
        this.clock = Objects.requireNonNull(clock, "clock");
        this.executor = Objects.requireNonNull(executor, "executor");
    }

    @EventListener(ApplicationReadyEvent.class)
    public void onApplicationReady() {
        try {
            executor.execute(this::warmUpReports);
        } catch (RuntimeException exception) {
            log.warn(
                    "exam_sprint_report_warmup_failed reportType={} stage={} failureReason={} exceptionType={} durationMs={}",
                    "ALL",
                    "async_submit",
                    exception.getClass().getSimpleName(),
                    exception.getClass().getSimpleName(),
                    0);
        }
    }

    void warmUpReports() {
        warmUpReport(ReportType.OUTLOOK, OUTLOOK_WARMUP_RESOURCE);
        warmUpReport(ReportType.ACHIEVEMENT, ACHIEVEMENT_WARMUP_RESOURCE);
    }

    private void warmUpReport(ReportType reportType, String resource) {
        long startedNanos = System.nanoTime();
        String stage = "load_payload";
        log.info("exam_sprint_report_warmup_started reportType={} resource={}", reportType, resource);

        try {
            JsonNode payload = loadPayload(resource);

            stage = "map_content";
            ReportContent content = contentOf(reportType, payload);

            stage = "renderer_selection";
            ExamSprintReportRenderer renderer = rendererFor(reportType);

            stage = "render_html";
            long renderStartedNanos = System.nanoTime();
            Instant generatedAt = clock.instant();
            String html = renderer.render(content, generatedAt);
            long renderDurationMs = elapsedMillis(renderStartedNanos);

            stage = "pdf_generation";
            long pdfStartedNanos = System.nanoTime();
            byte[] pdfBytes = pdfGenerator.generate(html);
            long pdfDurationMs = elapsedMillis(pdfStartedNanos);

            log.info(
                    "exam_sprint_report_warmup_succeeded reportType={} durationMs={} renderDurationMs={} pdfDurationMs={} htmlLength={} pdfByteLength={}",
                    reportType,
                    elapsedMillis(startedNanos),
                    renderDurationMs,
                    pdfDurationMs,
                    html.length(),
                    pdfBytes.length);
        } catch (Exception exception) {
            log.warn(
                    "exam_sprint_report_warmup_failed reportType={} stage={} failureReason={} exceptionType={} durationMs={}",
                    reportType,
                    stage,
                    exception.getClass().getSimpleName(),
                    exception.getClass().getSimpleName(),
                    elapsedMillis(startedNanos));
        }
    }

    private JsonNode loadPayload(String resource) throws IOException {
        InputStream inputStream = ExamSprintReportWarmupRunner.class.getClassLoader().getResourceAsStream(resource);
        if (inputStream == null) {
            throw new IllegalStateException("Warmup resource not found: " + resource);
        }
        try (inputStream) {
            return objectMapper.readTree(inputStream);
        }
    }

    private ReportContent contentOf(ReportType reportType, JsonNode payload) throws IOException {
        return switch (reportType) {
            case OUTLOOK -> new UnmodeledReportContent(ReportType.OUTLOOK, payload);
            case ACHIEVEMENT -> AchievementReportContentMapper.toDomainContent(
                    objectMapper.treeToValue(payload, AchievementExamSprintReportPayload.class));
        };
    }

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

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