package com.easyagents.document.pptx.mineru;

import com.easyagents.core.util.StringUtil;
import com.easyagents.document.core.async.DocumentAsyncTaskManager;
import com.easyagents.document.core.async.DocumentAsyncTaskRepository;
import com.easyagents.document.core.async.DocumentAsyncTaskUpdater;
import com.easyagents.document.core.async.InMemoryDocumentAsyncTaskRepository;
import com.easyagents.document.core.mineru.MineruClient;
import com.easyagents.document.core.mineru.MineruMapper;
import com.easyagents.document.core.mineru.MineruProperties;
import com.easyagents.document.core.entity.DocumentBlock;
import com.easyagents.document.core.entity.DocumentImage;
import com.easyagents.document.core.entity.DocumentPage;
import com.easyagents.document.core.entity.DocumentTable;
import com.easyagents.document.core.entity.ParseFile;
import com.easyagents.document.core.entity.ParseRequest;
import com.easyagents.document.core.entity.ParseResponse;
import com.easyagents.document.core.entity.ParseResult;
import com.easyagents.document.core.entity.PptxParseRequest;
import com.easyagents.document.core.support.AbstractAsyncDocumentParseService;
import com.easyagents.document.pptx.PptxDocumentProvider;
import com.easyagents.document.pptx.model.PptxParseArtifact;
import com.easyagents.document.pptx.model.PptxSlideArtifact;
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFSlide;

import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 基于 MinerU 的 PPTX 文档解析服务。
 *
 * @author Codex
 * @since 2026-04-16
 */
public class MineruPptxDocumentParseService extends AbstractAsyncDocumentParseService<PptxParseRequest> implements PptxDocumentProvider {

    public static final String PROVIDER_NAME = "mineru";

    private final MineruProperties properties;
    private final MineruClient client;
    private final MineruMapper mapper;

    /**
     * 创建服务实例。
     *
     * @param properties MinerU 配置
     */
    public MineruPptxDocumentParseService(MineruProperties properties) {
        this(properties, new MineruMapper(properties));
    }

    /**
     * 创建服务实例。
     *
     * @param properties MinerU 配置
     * @param mapper MinerU 映射器
     */
    public MineruPptxDocumentParseService(MineruProperties properties, MineruMapper mapper) {
        this(properties, new MineruClient(properties, mapper), mapper, defaultTaskManager());
    }

    /**
     * 创建服务实例。
     *
     * @param properties MinerU 配置
     * @param taskManager 异步任务管理器
     */
    public MineruPptxDocumentParseService(MineruProperties properties, DocumentAsyncTaskManager taskManager) {
        this(properties, new MineruMapper(properties), taskManager);
    }

    /**
     * 创建服务实例。
     *
     * @param properties MinerU 配置
     * @param mapper MinerU 映射器
     * @param taskManager 异步任务管理器
     */
    public MineruPptxDocumentParseService(MineruProperties properties,
                                          MineruMapper mapper,
                                          DocumentAsyncTaskManager taskManager) {
        this(properties, new MineruClient(properties, mapper), mapper, taskManager);
    }

    /**
     * 创建服务实例。
     *
     * @param properties MinerU 配置
     * @param client MinerU 客户端
     * @param mapper MinerU 映射器
     * @param taskManager 异步任务管理器
     */
    public MineruPptxDocumentParseService(MineruProperties properties,
                                          MineruClient client,
                                          MineruMapper mapper,
                                          DocumentAsyncTaskManager taskManager) {
        super(taskManager);
        this.properties = properties;
        this.client = client;
        this.mapper = mapper;
    }

    @Override
    public String getProvider() {
        return PROVIDER_NAME;
    }

    @Override
    protected PptxParseRequest normalizeRequest(ParseRequest request) {
        PptxParseRequest normalized = PptxParseRequest.from(request);
        if (normalized.getFiles() == null || normalized.getFiles().isEmpty()) {
            throw new IllegalArgumentException("PptxParseRequest files must not be empty");
        }
        normalized.setBackend(StringUtil.hasText(normalized.getBackend()) ? normalized.getBackend() : properties.getDefaultBackend());
        if (normalized.getLanguages() == null || normalized.getLanguages().isEmpty()) {
            normalized.setLanguages(new ArrayList<String>(properties.getDefaultLangList()));
        }
        normalized.setReturnMarkdown(normalized.getReturnMarkdown() == null ? Boolean.TRUE : normalized.getReturnMarkdown());
        normalized.setReturnMiddleJson(normalized.getReturnMiddleJson() == null ? Boolean.TRUE : normalized.getReturnMiddleJson());
        normalized.setReturnContentList(normalized.getReturnContentList() == null ? Boolean.TRUE : normalized.getReturnContentList());
        normalized.setReturnModelOutput(normalized.getReturnModelOutput() == null ? Boolean.FALSE : normalized.getReturnModelOutput());
        normalized.setReturnImages(normalized.getReturnImages() == null ? Boolean.TRUE : normalized.getReturnImages());
        normalized.setRenderScale(normalized.getRenderScale() == null || normalized.getRenderScale() <= 0 ? 2.0d : normalized.getRenderScale());
        normalized.setImageFormat(normalizeImageFormat(normalized.getImageFormat()));
        normalized.setIncludeSlideImageReference(
            normalized.getIncludeSlideImageReference() == null ? Boolean.TRUE : normalized.getIncludeSlideImageReference()
        );
        return normalized;
    }

    @Override
    protected ParseResponse doParse(PptxParseRequest request, DocumentAsyncTaskUpdater updater) {
        ParseResponse response = new ParseResponse();
        List<ParseResult> results = new ArrayList<ParseResult>();
        int totalSlides = countSlides(request);
        int processedSlides = 0;
        String backend = null;
        String version = null;

        for (ParseFile file : request.getFiles()) {
            ParseResult result = parseSinglePptx(file, request, updater, processedSlides, totalSlides);
            processedSlides += Integer.parseInt(String.valueOf(result.getMetadata().get("slideCount")));
            if (backend == null) {
                backend = (String) result.getMetadata().get("ocrBackend");
            }
            if (version == null) {
                version = (String) result.getMetadata().get("ocrVersion");
            }
            result.getMetadata().remove("slideCount");
            result.getMetadata().remove("ocrBackend");
            result.getMetadata().remove("ocrVersion");
            results.add(result);
        }
        response.setBackend(StringUtil.hasText(backend) ? backend : request.getBackend());
        response.setVersion(version);
        response.setResults(results);
        return response;
    }

    private ParseResult parseSinglePptx(ParseFile file,
                                        PptxParseRequest request,
                                        DocumentAsyncTaskUpdater updater,
                                        int processedSlidesBefore,
                                        int totalSlides) {
        ParseResult aggregate = new ParseResult();
        aggregate.setFileName(file.getFileName());
        StringBuilder markdownBuilder = new StringBuilder();
        PptxParseArtifact artifact = new PptxParseArtifact();
        String backend = null;
        String version = null;
        int slideCount = 0;

        try (XMLSlideShow slideShow = new XMLSlideShow(new ByteArrayInputStream(file.getContent()))) {
            List<XSLFSlide> slides = slideShow.getSlides();
            Dimension pageSize = slideShow.getPageSize();
            int startSlide = request.getStartSlideIndex() == null ? 0 : Math.max(request.getStartSlideIndex(), 0);
            int endSlide = request.getEndSlideIndex() == null
                ? slides.size() - 1
                : Math.min(request.getEndSlideIndex(), slides.size() - 1);
            if (endSlide < startSlide) {
                endSlide = startSlide - 1;
            }

            for (int slideIndex = startSlide; slideIndex <= endSlide; slideIndex++) {
                XSLFSlide slide = slides.get(slideIndex);
                slideCount++;
                updateProgress(updater, "extracting", processedSlidesBefore + slideCount - 1, totalSlides,
                    "正在渲染第 " + (slideIndex + 1) + " 页幻灯片");

                byte[] imageBytes = renderSlide(slide, pageSize, request.getRenderScale(), request.getImageFormat());
                String imagePath = buildImagePath(slideIndex, request.getImageFormat());
                String imageName = buildImageName(slideIndex);

                updateProgress(updater, "ocr", processedSlidesBefore + slideCount - 1, totalSlides,
                    "正在识别第 " + (slideIndex + 1) + " 页幻灯片");
                ParseResult ocrResult = parseSlideImage(slideIndex, imageBytes, request, imagePath);

                if (!StringUtil.hasText(backend)) {
                    backend = (String) ocrResult.getMetadata().get("middleBackend");
                }
                if (!StringUtil.hasText(version)) {
                    version = (String) ocrResult.getMetadata().get("middleVersion");
                }

                appendSlideMarkdown(markdownBuilder, slideIndex, imageName, imagePath, request, ocrResult.getMarkdown());
                aggregate.getImages().add(buildSlideImage(slideIndex, imageName, imagePath, request.getImageFormat(), imageBytes));
                aggregate.getPages().add(buildPage(slideIndex, pageSize, request.getRenderScale()));
                mergeOcrResult(aggregate, slideIndex, ocrResult);
                artifact.getSlides().add(buildSlideArtifact(slideIndex, slide, imageName, imagePath, ocrResult));
            }
        } catch (IOException exception) {
            throw new IllegalStateException("Failed to parse PPTX file: " + file.getFileName(), exception);
        }

        updateProgress(updater, "assembling", processedSlidesBefore + slideCount, totalSlides, "正在汇总 PPTX 解析结果");
        aggregate.setMarkdown(markdownBuilder.toString().trim());
        aggregate.setPlainText(aggregate.getMarkdown());
        aggregate.getArtifacts().getExtraJsonArtifacts().put("pptx", artifact);
        aggregate.getMetadata().put("slideCount", slideCount);
        aggregate.getMetadata().put("ocrBackend", backend);
        aggregate.getMetadata().put("ocrVersion", version);
        return aggregate;
    }

    private ParseResult parseSlideImage(int slideIndex, byte[] imageBytes, PptxParseRequest request, String imagePath) {
        ParseRequest imageRequest = new ParseRequest();
        imageRequest.addFile(ParseFile.of("slide-" + (slideIndex + 1) + "." + request.getImageFormat(), imageBytes, "image/" + request.getImageFormat()));
        imageRequest.setBackend(request.getBackend());
        imageRequest.setLanguages(request.getLanguages());
        imageRequest.setReturnMarkdown(true);
        imageRequest.setReturnMiddleJson(true);
        imageRequest.setReturnContentList(true);
        imageRequest.setReturnModelOutput(false);
        imageRequest.setReturnImages(false);
        ParseResponse response = mapper.toParseResponse(client.parse(imageRequest));
        ParseResult result = response.getResults().isEmpty() ? new ParseResult() : response.getResults().get(0);
        if (!StringUtil.hasText(result.getMarkdown())) {
            result.setMarkdown(result.getPlainText());
        }
        result.getMetadata().put("slideImagePath", imagePath);
        return result;
    }

    private void appendSlideMarkdown(StringBuilder markdownBuilder,
                                     int slideIndex,
                                     String imageName,
                                     String imagePath,
                                     PptxParseRequest request,
                                     String ocrMarkdown) {
        if (markdownBuilder.length() > 0) {
            markdownBuilder.append("\n\n");
        }
        markdownBuilder.append("# Slide ").append(slideIndex + 1).append("\n\n");
        if (Boolean.TRUE.equals(request.getIncludeSlideImageReference())) {
            markdownBuilder.append("![").append(imageName).append("](").append(imagePath).append(")\n\n");
        }
        if (StringUtil.hasText(ocrMarkdown)) {
            markdownBuilder.append(ocrMarkdown.trim());
        }
    }

    private DocumentImage buildSlideImage(int slideIndex, String imageName, String imagePath, String imageFormat, byte[] imageBytes) {
        DocumentImage image = new DocumentImage();
        image.setPageIndex(slideIndex);
        image.setName(imageName);
        image.setSourcePath(imagePath);
        image.setMimeType("image/" + imageFormat);
        image.setContent(imageBytes);
        return image;
    }

    private DocumentPage buildPage(int slideIndex, Dimension pageSize, Double renderScale) {
        DocumentPage page = new DocumentPage();
        page.setPageIndex(slideIndex);
        page.setWidth(pageSize.getWidth() * renderScale);
        page.setHeight(pageSize.getHeight() * renderScale);
        return page;
    }

    private void mergeOcrResult(ParseResult aggregate, int slideIndex, ParseResult ocrResult) {
        for (DocumentBlock block : ocrResult.getBlocks()) {
            block.setPageIndex(slideIndex);
            aggregate.getBlocks().add(block);
        }
        for (DocumentTable table : ocrResult.getTables()) {
            table.setPageIndex(slideIndex);
            aggregate.getTables().add(table);
        }
        for (String warning : ocrResult.getWarnings()) {
            aggregate.getWarnings().add("Slide " + (slideIndex + 1) + ": " + warning);
        }
    }

    private PptxSlideArtifact buildSlideArtifact(int slideIndex,
                                                 XSLFSlide slide,
                                                 String imageName,
                                                 String imagePath,
                                                 ParseResult ocrResult) {
        PptxSlideArtifact artifact = new PptxSlideArtifact();
        artifact.setSlideIndex(slideIndex);
        artifact.setTitle(slide.getTitle());
        artifact.setImageName(imageName);
        artifact.setImagePath(imagePath);
        artifact.setOcrMarkdown(ocrResult.getMarkdown());
        artifact.setMiddleJson(ocrResult.getArtifacts().getMiddleJson());
        artifact.setContentList(ocrResult.getArtifacts().getContentList());
        artifact.setWarnings(new ArrayList<String>(ocrResult.getWarnings()));
        return artifact;
    }

    private byte[] renderSlide(XSLFSlide slide, Dimension pageSize, Double renderScale, String imageFormat) throws IOException {
        double scale = renderScale == null ? 2.0d : renderScale;
        int width = Math.max(1, (int) Math.round(pageSize.getWidth() * scale));
        int height = Math.max(1, (int) Math.round(pageSize.getHeight() * scale));
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = image.createGraphics();
        try {
            graphics.setColor(Color.WHITE);
            graphics.fillRect(0, 0, width, height);
            graphics.scale(scale, scale);
            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            slide.draw(graphics);
        } finally {
            graphics.dispose();
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(image, imageFormat, outputStream);
        return outputStream.toByteArray();
    }

    private int countSlides(PptxParseRequest request) {
        int totalSlides = 0;
        for (ParseFile file : request.getFiles()) {
            try (XMLSlideShow slideShow = new XMLSlideShow(new ByteArrayInputStream(file.getContent()))) {
                int slideSize = slideShow.getSlides().size();
                int startSlide = request.getStartSlideIndex() == null ? 0 : Math.max(request.getStartSlideIndex(), 0);
                int endSlide = request.getEndSlideIndex() == null
                    ? slideSize - 1
                    : Math.min(request.getEndSlideIndex(), slideSize - 1);
                if (endSlide >= startSlide) {
                    totalSlides += endSlide - startSlide + 1;
                }
            } catch (IOException exception) {
                throw new IllegalStateException("Failed to inspect PPTX slide count: " + file.getFileName(), exception);
            }
        }
        return totalSlides;
    }

    private void updateProgress(DocumentAsyncTaskUpdater updater,
                                String stage,
                                int processedItems,
                                int totalItems,
                                String message) {
        if (updater == null) {
            return;
        }
        int safeTotal = totalItems <= 0 ? 1 : totalItems;
        int percent = (int) Math.min(99, Math.round(processedItems * 100.0d / safeTotal));
        updater.update(stage, percent, processedItems, totalItems, message);
    }

    private String normalizeImageFormat(String imageFormat) {
        if ("jpg".equalsIgnoreCase(imageFormat) || "jpeg".equalsIgnoreCase(imageFormat)) {
            return "jpg";
        }
        return "png";
    }

    private String buildImagePath(int slideIndex, String imageFormat) {
        return "images/slide-" + formatIndex(slideIndex) + "/page." + imageFormat;
    }

    private String buildImageName(int slideIndex) {
        return "slide-" + formatIndex(slideIndex) + "-page";
    }

    private String formatIndex(int slideIndex) {
        int displayIndex = slideIndex + 1;
        if (displayIndex < 10) {
            return "00" + displayIndex;
        }
        if (displayIndex < 100) {
            return "0" + displayIndex;
        }
        return String.valueOf(displayIndex);
    }

    private static DocumentAsyncTaskManager defaultTaskManager() {
        DocumentAsyncTaskRepository repository = new InMemoryDocumentAsyncTaskRepository();
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        return new DocumentAsyncTaskManager(repository, executorService);
    }
}
