package com.easyagents.document.core.mineru;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.core.util.StringUtil;
import com.easyagents.document.core.exception.DocumentParseException;
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.PdfParseRequest;
import com.easyagents.document.core.entity.ParseArtifacts;
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.ParseTaskStatus;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * MinerU 原始协议与统一模型之间的映射器。
 *
 * @author Codex
 * @since 2026-04-16
 */
public class MineruMapper {

    private final MineruProperties properties;

    /**
     * 创建映射器。
     *
     * @param properties MinerU 配置
     */
    public MineruMapper(MineruProperties properties) {
        this.properties = properties;
    }

    /**
     * 构建同步请求表单字段。
     *
     * @param request 解析请求
     * @return 表单字段
     */
    public Map<String, List<String>> buildSyncFormFields(ParseRequest request) {
        Map<String, List<String>> fields = buildBaseFormFields(request);
        putSingleValue(fields, "return_md", String.valueOf(isTrue(request.getReturnMarkdown())));
        putSingleValue(fields, "return_middle_json", String.valueOf(isTrue(request.getReturnMiddleJson())));
        putSingleValue(fields, "return_content_list", String.valueOf(isTrue(request.getReturnContentList())));
        putSingleValue(fields, "return_model_output", String.valueOf(isTrue(request.getReturnModelOutput())));
        putSingleValue(fields, "return_images", String.valueOf(isTrue(request.getReturnImages())));
        putSingleValue(fields, "response_format_zip", "false");
        return fields;
    }

    /**
     * 构建异步请求表单字段。
     *
     * @param request 解析请求
     * @return 表单字段
     */
    public Map<String, List<String>> buildAsyncFormFields(ParseRequest request) {
        Map<String, List<String>> fields = buildBaseFormFields(request);
        putSingleValue(fields, "return_md", "true");
        putSingleValue(fields, "return_middle_json", "true");
        putSingleValue(fields, "return_content_list", "true");
        putSingleValue(fields, "return_model_output", "true");
        putSingleValue(fields, "return_images", "true");
        putSingleValue(fields, "response_format_zip", "true");
        return fields;
    }

    /**
     * 将原始 JSON 转为 MinerU 任务状态 DTO。
     *
     * @param jsonObject 原始 JSON
     * @return 任务状态 DTO
     */
    public MineruTaskStatus toTaskStatus(JSONObject jsonObject) {
        MineruTaskStatus taskStatus = new MineruTaskStatus();
        taskStatus.setTaskId(jsonObject.getString("task_id"));
        taskStatus.setStatus(jsonObject.getString("status"));
        taskStatus.setBackend(jsonObject.getString("backend"));
        taskStatus.setFileNames(toStringList(jsonObject.getJSONArray("file_names")));
        taskStatus.setCreatedAt(jsonObject.getString("created_at"));
        taskStatus.setStartedAt(jsonObject.getString("started_at"));
        taskStatus.setCompletedAt(jsonObject.getString("completed_at"));
        taskStatus.setError(jsonObject.getString("error"));
        taskStatus.setStatusUrl(jsonObject.getString("status_url"));
        taskStatus.setResultUrl(jsonObject.getString("result_url"));
        taskStatus.setQueuedAhead(jsonObject.getInteger("queued_ahead"));
        taskStatus.setVersion(jsonObject.getString("version"));
        taskStatus.setMessage(jsonObject.getString("message"));
        return taskStatus;
    }

    /**
     * 将原始 JSON 转为 MinerU 结果 DTO。
     *
     * @param jsonObject 原始 JSON
     * @return 结果 DTO
     */
    public MineruResultPayload toResultPayload(JSONObject jsonObject) {
        MineruResultPayload payload = new MineruResultPayload();
        payload.setBackend(jsonObject.getString("backend"));
        payload.setVersion(jsonObject.getString("version"));
        Map<String, JSONObject> results = new LinkedHashMap<String, JSONObject>();
        JSONObject resultJson = MineruJsonSupport.asObject(jsonObject.get("results"), "MinerU sync results");
        if (resultJson != null) {
            for (String key : resultJson.keySet()) {
                JSONObject result = MineruJsonSupport.asObject(
                    resultJson.get(key),
                    "MinerU sync result entry: " + key
                );
                if (result != null) {
                    results.put(key, result);
                }
            }
        }
        payload.setResults(results);
        return payload;
    }

    /**
     * 将 MinerU 任务状态转为统一模型。
     *
     * @param taskStatus 原始任务状态
     * @return 统一任务状态
     */
    public ParseTaskStatus toParseTaskStatus(MineruTaskStatus taskStatus) {
        ParseTaskStatus status = new ParseTaskStatus();
        status.setTaskId(taskStatus.getTaskId());
        status.setStatus(taskStatus.getStatus());
        status.setBackend(taskStatus.getBackend());
        status.setFileNames(taskStatus.getFileNames());
        status.setCreatedAt(taskStatus.getCreatedAt());
        status.setStartedAt(taskStatus.getStartedAt());
        status.setCompletedAt(taskStatus.getCompletedAt());
        status.setError(taskStatus.getError());
        status.setStatusUrl(taskStatus.getStatusUrl());
        status.setResultUrl(taskStatus.getResultUrl());
        status.setQueuedAhead(taskStatus.getQueuedAhead());
        return status;
    }

    /**
     * 将同步 JSON 结果转为统一响应。
     *
     * @param payload MinerU 结果 DTO
     * @return 统一响应
     */
    public ParseResponse toParseResponse(MineruResultPayload payload) {
        ParseResponse response = new ParseResponse();
        response.setBackend(payload.getBackend());
        response.setVersion(payload.getVersion());
        List<ParseResult> parseResults = new ArrayList<ParseResult>();
        for (Map.Entry<String, JSONObject> entry : payload.getResults().entrySet()) {
            parseResults.add(mapSingleResult(entry.getKey(), entry.getValue()));
        }
        response.setResults(parseResults);
        return response;
    }

    /**
     * 将 ZIP 结果转为统一响应。
     *
     * @param zipBytes ZIP 二进制
     * @return 统一响应
     */
    public ParseResponse fromZip(byte[] zipBytes) {
        Map<String, ZipArtifactBundle> bundles = unzip(zipBytes);
        if (bundles.isEmpty()) {
            throw new DocumentParseException("MinerU ZIP result does not contain any parse artifacts");
        }
        ParseResponse response = new ParseResponse();
        List<ParseResult> parseResults = new ArrayList<ParseResult>();
        for (Map.Entry<String, ZipArtifactBundle> entry : bundles.entrySet()) {
            parseResults.add(mapZipBundle(entry.getKey(), entry.getValue()));
        }
        response.setResults(parseResults);
        return response;
    }

    /**
     * 使用异步任务状态和 ZIP 内部工件回填响应元数据。
     *
     * @param response 统一响应
     * @param backend 任务状态中的 backend
     * @param version 任务状态中的 version
     */
    public void enrichAsyncResponse(ParseResponse response, String backend, String version) {
        if (response == null) {
            return;
        }
        response.setBackend(StringUtil.hasText(backend) ? backend : resolveBackendFromResults(response));
        String resolvedVersion = StringUtil.hasText(version) ? version : resolveVersionFromResults(response);
        response.setVersion(resolvedVersion);
    }

    private Map<String, List<String>> buildBaseFormFields(ParseRequest request) {
        Map<String, List<String>> fields = new LinkedHashMap<String, List<String>>();
        putSingleValue(fields, "backend", StringUtil.hasText(request.getBackend()) ? request.getBackend() : properties.getDefaultBackend());
        List<String> languages = request.getLanguages();
        if (languages == null || languages.isEmpty()) {
            languages = properties.getDefaultLangList();
        }
        if (languages != null && !languages.isEmpty()) {
            fields.put("lang_list", new ArrayList<String>(languages));
        }
        if (request instanceof PdfParseRequest) {
            PdfParseRequest pdfParseRequest = (PdfParseRequest) request;
            putSingleValue(fields, "parse_method",
                StringUtil.hasText(pdfParseRequest.getParseMethod()) ? pdfParseRequest.getParseMethod() : properties.getDefaultParseMethod());
            putSingleValue(fields, "formula_enable",
                String.valueOf(boolOrDefault(pdfParseRequest.getFormulaEnabled(), properties.getDefaultFormulaEnable())));
            putSingleValue(fields, "table_enable",
                String.valueOf(boolOrDefault(pdfParseRequest.getTableEnabled(), properties.getDefaultTableEnable())));
            putSingleValue(fields, "start_page_id", String.valueOf(intOrDefault(pdfParseRequest.getStartPageIndex(), 0)));
            putSingleValue(fields, "end_page_id", String.valueOf(intOrDefault(pdfParseRequest.getEndPageIndex(), 99999)));
        }
        return fields;
    }

    private void putSingleValue(Map<String, List<String>> fields, String key, String value) {
        List<String> values = new ArrayList<String>(1);
        values.add(value);
        fields.put(key, values);
    }

    private ParseResult mapSingleResult(String fileName, JSONObject fileResult) {
        ParseResult result = new ParseResult();
        result.setFileName(fileName);
        result.setMarkdown(fileResult.getString("md_content"));
        result.setPlainText(result.getMarkdown());

        ParseArtifacts artifacts = new ParseArtifacts();
        artifacts.setMiddleJson(MineruJsonSupport.normalizeValue(
            fileResult.get("middle_json"),
            "MinerU result " + fileName + " middle_json"
        ));
        artifacts.setContentList(MineruJsonSupport.normalizeValue(
            fileResult.get("content_list"),
            "MinerU result " + fileName + " content_list"
        ));
        artifacts.setModelOutput(MineruJsonSupport.normalizeValueIfJsonLike(
            fileResult.get("model_output"),
            "MinerU result " + fileName + " model_output"
        ));
        result.setArtifacts(artifacts);

        Map<String, String> imageDataUrls = toStringMap(MineruJsonSupport.asObject(
            fileResult.get("images"),
            "MinerU result " + fileName + " images"
        ));
        Map<String, byte[]> imageContents = toBinaryMap(imageDataUrls);
        applyStructuredArtifacts(result, imageDataUrls, imageContents);
        if (result.getMarkdown() == null && result.getArtifacts().getMiddleJson() == null && result.getArtifacts().getContentList() == null) {
            result.getWarnings().add("MinerU did not return markdown, middle_json or content_list");
        }
        return result;
    }

    private ParseResult mapZipBundle(String fileName, ZipArtifactBundle bundle) {
        ParseResult result = new ParseResult();
        result.setFileName(fileName);

        String markdown = firstText(bundle.entriesBySuffix, ".md");
        result.setMarkdown(markdown);
        result.setPlainText(markdown);

        ParseArtifacts artifacts = new ParseArtifacts();
        Object middleArtifact = firstJsonValue(bundle.entriesBySuffix, "_middle.json");
        Object contentListArtifact = firstJsonValue(bundle.entriesBySuffix, "_content_list.json");
        Object modelOutputArtifact = firstJsonValue(bundle.entriesBySuffix, "_model.json");

        JSONObject middleJson = asObject(middleArtifact, "MinerU ZIP middle artifact: " + fileName);
        JSONArray contentList = asArray(contentListArtifact, "MinerU ZIP content_list artifact: " + fileName);
        Object modelOutput = modelOutputArtifact;

        if (contentList == null && middleArtifact instanceof JSONArray) {
            contentList = (JSONArray) middleArtifact;
            middleJson = null;
            middleArtifact = null;
        }
        if (contentList == null && modelOutputArtifact instanceof JSONArray) {
            contentList = (JSONArray) modelOutputArtifact;
        }

        artifacts.setMiddleJson(middleArtifact);
        artifacts.setContentList(contentList == null ? contentListArtifact : contentList);
        artifacts.setModelOutput(modelOutput);

        JSONArray contentListV2 = asArray(
            firstJsonValue(bundle.entriesBySuffix, "_content_list_v2.json"),
            "MinerU ZIP content_list_v2 artifact: " + fileName
        );
        if (contentListV2 != null) {
            artifacts.getExtraJsonArtifacts().put("contentListV2", contentListV2);
        }

        for (Map.Entry<String, byte[]> entry : bundle.otherBinaryEntries.entrySet()) {
            artifacts.getExtraBinaryArtifacts().put(entry.getKey(), entry.getValue());
        }
        result.setArtifacts(artifacts);

        Map<String, String> imageDataUrls = new LinkedHashMap<String, String>();
        Map<String, byte[]> imageContents = new LinkedHashMap<String, byte[]>();
        for (Map.Entry<String, byte[]> imageEntry : bundle.images.entrySet()) {
            imageDataUrls.put(imageEntry.getKey(), toDataUrl(imageEntry.getKey(), imageEntry.getValue()));
            imageContents.put(imageEntry.getKey(), imageEntry.getValue());
        }
        applyStructuredArtifacts(result, imageDataUrls, imageContents);

        if (markdown == null && middleJson == null && contentList == null) {
            throw new DocumentParseException("MinerU ZIP result missing critical artifacts for file: " + fileName);
        }
        return result;
    }

    private void applyStructuredArtifacts(ParseResult result, Map<String, String> imageDataUrls, Map<String, byte[]> imageContents) {
        JSONObject middleJson = asObject(result.getArtifacts().getMiddleJson(), "MinerU middle_json artifact");
        JSONArray contentList = asArray(result.getArtifacts().getContentList(), "MinerU content_list artifact");

        if (middleJson != null) {
            fillPages(result, middleJson);
            result.getMetadata().put("middleBackend", middleJson.getString("_backend"));
            result.getMetadata().put("middleVersion", middleJson.getString("_version_name"));
        }

        if (contentList != null) {
            fillFromContentList(result, contentList, imageDataUrls, imageContents);
        } else if (middleJson != null) {
            fillFromMiddleJson(result, middleJson, imageDataUrls, imageContents);
        }

        if ((result.getImages() == null || result.getImages().isEmpty()) && imageDataUrls != null && !imageDataUrls.isEmpty()) {
            for (Map.Entry<String, String> entry : imageDataUrls.entrySet()) {
                DocumentImage image = new DocumentImage();
                image.setName(baseName(entry.getKey()));
                image.setSourcePath(entry.getKey());
                image.setDataUrl(entry.getValue());
                image.setContent(matchBinaryContent(entry.getKey(), imageContents));
                image.setMimeType(detectMimeType(entry.getKey()));
                result.getImages().add(image);
            }
        }
    }

    private void fillPages(ParseResult result, JSONObject middleJson) {
        JSONArray pdfInfo = middleJson.getJSONArray("pdf_info");
        if (pdfInfo == null) {
            return;
        }
        List<DocumentPage> pages = new ArrayList<DocumentPage>();
        for (int index = 0; index < pdfInfo.size(); index++) {
            JSONObject pageJson = pdfInfo.getJSONObject(index);
            DocumentPage page = new DocumentPage();
            page.setPageIndex(pageJson.getInteger("page_idx"));
            JSONArray pageSize = pageJson.getJSONArray("page_size");
            if (pageSize != null && pageSize.size() >= 2) {
                page.setWidth(pageSize.getDouble(0));
                page.setHeight(pageSize.getDouble(1));
            }
            page.getMetadata().put("raw", pageJson);
            pages.add(page);
        }
        result.setPages(pages);
    }

    private void fillFromContentList(ParseResult result,
                                     JSONArray contentList,
                                     Map<String, String> imageDataUrls,
                                     Map<String, byte[]> imageContents) {
        for (int index = 0; index < contentList.size(); index++) {
            JSONObject item = contentList.getJSONObject(index);
            if (item == null) {
                continue;
            }
            DocumentBlock block = new DocumentBlock();
            block.setType(item.getString("type"));
            block.setPageIndex(item.getInteger("page_idx"));
            block.setBoundingBox(toDoubleList(item.getJSONArray("bbox")));
            Integer blockLevel = item.getInteger("text_level");
            if (blockLevel == null) {
                blockLevel = item.getInteger("level");
            }
            block.setLevel(blockLevel);
            block.setText(extractBlockText(item));
            block.setHtml(item.getString("table_body"));
            block.setImagePath(item.getString("img_path"));
            block.getMetadata().put("raw", item);
            result.getBlocks().add(block);

            if ("table".equals(item.getString("type"))) {
                DocumentTable table = new DocumentTable();
                table.setPageIndex(item.getInteger("page_idx"));
                table.setBoundingBox(toDoubleList(item.getJSONArray("bbox")));
                table.setHtml(item.getString("table_body"));
                table.setImagePath(item.getString("img_path"));
                table.setCaptions(toStringList(item.getJSONArray("table_caption")));
                table.setFootnotes(toStringList(item.getJSONArray("table_footnote")));
                result.getTables().add(table);
            }

            if (isVisualType(item.getString("type"))) {
                DocumentImage image = new DocumentImage();
                image.setPageIndex(item.getInteger("page_idx"));
                image.setBoundingBox(toDoubleList(item.getJSONArray("bbox")));
                image.setSourcePath(item.getString("img_path"));
                image.setName(baseName(item.getString("img_path")));
                image.setMimeType(detectMimeType(item.getString("img_path")));
                image.setCaptions(extractCaptions(item));
                image.setFootnotes(extractFootnotes(item));
                image.setDataUrl(matchDataUrl(item.getString("img_path"), imageDataUrls));
                image.setContent(matchBinaryContent(item.getString("img_path"), imageContents));
                result.getImages().add(image);
            }
        }
    }

    private void fillFromMiddleJson(ParseResult result,
                                    JSONObject middleJson,
                                    Map<String, String> imageDataUrls,
                                    Map<String, byte[]> imageContents) {
        JSONArray pages = middleJson.getJSONArray("pdf_info");
        if (pages == null) {
            return;
        }
        for (int pageIndex = 0; pageIndex < pages.size(); pageIndex++) {
            JSONObject page = pages.getJSONObject(pageIndex);
            fillBlocksFromMiddlePage(result, page.getJSONArray("para_blocks"), page.getInteger("page_idx"));
            fillVisualsFromMiddlePage(result, page.getJSONArray("tables"), page.getInteger("page_idx"), true, imageDataUrls, imageContents);
            fillVisualsFromMiddlePage(result, page.getJSONArray("images"), page.getInteger("page_idx"), false, imageDataUrls, imageContents);
        }
    }

    private void fillBlocksFromMiddlePage(ParseResult result, JSONArray blocks, Integer pageIndex) {
        if (blocks == null) {
            return;
        }
        for (int index = 0; index < blocks.size(); index++) {
            JSONObject blockJson = blocks.getJSONObject(index);
            if (blockJson == null) {
                continue;
            }
            DocumentBlock block = new DocumentBlock();
            block.setType(blockJson.getString("type"));
            block.setPageIndex(pageIndex);
            block.setBoundingBox(toDoubleList(blockJson.getJSONArray("bbox")));
            block.setText(extractTextFromMiddleBlock(blockJson));
            block.setImagePath(extractImagePathFromMiddleBlock(blockJson));
            block.getMetadata().put("raw", blockJson);
            result.getBlocks().add(block);
        }
    }

    private void fillVisualsFromMiddlePage(ParseResult result,
                                           JSONArray blocks,
                                           Integer pageIndex,
                                           boolean table,
                                           Map<String, String> imageDataUrls,
                                           Map<String, byte[]> imageContents) {
        if (blocks == null) {
            return;
        }
        for (int index = 0; index < blocks.size(); index++) {
            JSONObject blockJson = blocks.getJSONObject(index);
            if (blockJson == null) {
                continue;
            }
            if (table) {
                DocumentTable documentTable = new DocumentTable();
                documentTable.setPageIndex(pageIndex);
                documentTable.setBoundingBox(toDoubleList(blockJson.getJSONArray("bbox")));
                documentTable.setCaptions(extractTextsByType(blockJson, "table_caption"));
                documentTable.setFootnotes(extractTextsByType(blockJson, "table_footnote"));
                documentTable.setImagePath(extractImagePathByType(blockJson, "table_body"));
                result.getTables().add(documentTable);
            } else {
                DocumentImage documentImage = new DocumentImage();
                documentImage.setPageIndex(pageIndex);
                documentImage.setBoundingBox(toDoubleList(blockJson.getJSONArray("bbox")));
                documentImage.setCaptions(extractTextsByType(blockJson, "image_caption"));
                documentImage.setFootnotes(extractTextsByType(blockJson, "image_footnote"));
                documentImage.setSourcePath(extractImagePathByType(blockJson, "image_body"));
                documentImage.setName(baseName(documentImage.getSourcePath()));
                documentImage.setMimeType(detectMimeType(documentImage.getSourcePath()));
                documentImage.setDataUrl(matchDataUrl(documentImage.getSourcePath(), imageDataUrls));
                documentImage.setContent(matchBinaryContent(documentImage.getSourcePath(), imageContents));
                result.getImages().add(documentImage);
            }
        }
    }

    private String resolveBackendFromResults(ParseResponse response) {
        if (response.getResults() == null || response.getResults().isEmpty()) {
            return properties.getDefaultBackend();
        }
        for (ParseResult result : response.getResults()) {
            Object middleBackend = result.getMetadata().get("middleBackend");
            if (middleBackend instanceof String && StringUtil.hasText((String) middleBackend)) {
                return (String) middleBackend;
            }
        }
        return properties.getDefaultBackend();
    }

    private String resolveVersionFromResults(ParseResponse response) {
        if (response.getResults() == null || response.getResults().isEmpty()) {
            return null;
        }
        for (ParseResult result : response.getResults()) {
            Object middleVersion = result.getMetadata().get("middleVersion");
            if (middleVersion instanceof String && StringUtil.hasText((String) middleVersion)) {
                return (String) middleVersion;
            }
        }
        return null;
    }

    private Map<String, ZipArtifactBundle> unzip(byte[] zipBytes) {
        Map<String, ZipArtifactBundle> bundles = new LinkedHashMap<String, ZipArtifactBundle>();
        try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
            ZipEntry entry;
            while ((entry = zipInputStream.getNextEntry()) != null) {
                if (entry.isDirectory()) {
                    continue;
                }
                byte[] entryBytes = readBytes(zipInputStream);
                String entryName = entry.getName();
                String fileName = resolveFileName(entryName);
                ZipArtifactBundle bundle = bundles.get(fileName);
                if (bundle == null) {
                    bundle = new ZipArtifactBundle();
                    bundles.put(fileName, bundle);
                }
                if (entryName.contains("/images/")) {
                    bundle.images.put(entryName, entryBytes);
                } else if (entryName.endsWith(".md")
                    || entryName.endsWith("_middle.json")
                    || entryName.endsWith("_content_list.json")
                    || entryName.endsWith("_content_list_v2.json")
                    || entryName.endsWith("_model.json")) {
                    bundle.entriesBySuffix.put(entryName, entryBytes);
                } else {
                    bundle.otherBinaryEntries.put(entryName, entryBytes);
                }
            }
        } catch (IOException exception) {
            throw new DocumentParseException("Failed to unzip MinerU result", exception);
        }
        return bundles;
    }

    private byte[] readBytes(ZipInputStream zipInputStream) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[8192];
        int length;
        while ((length = zipInputStream.read(buffer)) >= 0) {
            outputStream.write(buffer, 0, length);
        }
        return outputStream.toByteArray();
    }

    private String resolveFileName(String entryName) {
        String[] segments = entryName.split("/");
        if (segments.length > 0 && StringUtil.hasText(segments[0])) {
            return segments[0];
        }
        String fileName = baseName(entryName);
        int dotIndex = fileName.indexOf('.');
        return dotIndex > 0 ? fileName.substring(0, dotIndex) : fileName;
    }

    private String firstText(Map<String, byte[]> entries, String suffix) {
        for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
            if (entry.getKey().endsWith(suffix)) {
                return new String(entry.getValue());
            }
        }
        return null;
    }

    private Object firstJsonValue(Map<String, byte[]> entries, String suffix) {
        String text = firstText(entries, suffix);
        if (!StringUtil.hasText(text)) {
            return null;
        }
        return MineruJsonSupport.parseValue(text, "MinerU ZIP artifact " + suffix);
    }

    private JSONObject asObject(Object value, String context) {
        if (value instanceof JSONArray) {
            return null;
        }
        return MineruJsonSupport.asObject(value, context);
    }

    private JSONArray asArray(Object value, String context) {
        return MineruJsonSupport.asArray(value, context);
    }

    private List<String> toStringList(JSONArray jsonArray) {
        if (jsonArray == null || jsonArray.isEmpty()) {
            return new ArrayList<String>();
        }
        List<String> values = new ArrayList<String>();
        for (int index = 0; index < jsonArray.size(); index++) {
            values.add(jsonArray.getString(index));
        }
        return values;
    }

    private Map<String, String> toStringMap(JSONObject jsonObject) {
        if (jsonObject == null || jsonObject.isEmpty()) {
            return new LinkedHashMap<String, String>();
        }
        Map<String, String> values = new LinkedHashMap<String, String>();
        for (String key : jsonObject.keySet()) {
            values.put(key, jsonObject.getString(key));
        }
        return values;
    }

    private Map<String, byte[]> toBinaryMap(Map<String, String> dataUrls) {
        Map<String, byte[]> values = new LinkedHashMap<String, byte[]>();
        if (dataUrls == null || dataUrls.isEmpty()) {
            return values;
        }
        for (Map.Entry<String, String> entry : dataUrls.entrySet()) {
            byte[] content = decodeDataUrl(entry.getValue());
            if (content != null) {
                values.put(entry.getKey(), content);
            }
        }
        return values;
    }

    private List<Double> toDoubleList(JSONArray jsonArray) {
        if (jsonArray == null || jsonArray.isEmpty()) {
            return new ArrayList<Double>();
        }
        List<Double> values = new ArrayList<Double>();
        for (int index = 0; index < jsonArray.size(); index++) {
            values.add(jsonArray.getDouble(index));
        }
        return values;
    }

    private List<String> extractCaptions(JSONObject item) {
        List<String> texts = new ArrayList<String>();
        texts.addAll(toStringList(item.getJSONArray("image_caption")));
        texts.addAll(toStringList(item.getJSONArray("table_caption")));
        return texts;
    }

    private List<String> extractFootnotes(JSONObject item) {
        List<String> texts = new ArrayList<String>();
        texts.addAll(toStringList(item.getJSONArray("image_footnote")));
        texts.addAll(toStringList(item.getJSONArray("table_footnote")));
        return texts;
    }

    private boolean isVisualType(String type) {
        return "image".equals(type) || "table".equals(type) || "chart".equals(type) || "seal".equals(type);
    }

    private String extractBlockText(JSONObject item) {
        String type = item.getString("type");
        if ("text".equals(type) || "header".equals(type) || "footer".equals(type)
            || "page_number".equals(type) || "aside_text".equals(type) || "page_footnote".equals(type)
            || "equation".equals(type) || "title".equals(type)) {
            String text = item.getString("text");
            return StringUtil.hasText(text) ? text : item.getString("content");
        }
        if ("list".equals(type)) {
            return joinList(toStringList(item.getJSONArray("list_items")));
        }
        if ("code".equals(type)) {
            return item.getString("code_body");
        }
        if ("image".equals(type)) {
            return joinList(toStringList(item.getJSONArray("image_caption")));
        }
        if ("table".equals(type)) {
            String tableCaption = joinList(toStringList(item.getJSONArray("table_caption")));
            return StringUtil.hasText(tableCaption) ? tableCaption : item.getString("content");
        }
        String text = item.getString("text");
        return StringUtil.hasText(text) ? text : item.getString("content");
    }

    private String extractTextFromMiddleBlock(JSONObject blockJson) {
        List<String> texts = new ArrayList<String>();
        JSONArray blocks = blockJson.getJSONArray("blocks");
        if (blocks == null) {
            return null;
        }
        for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) {
            JSONObject childBlock = blocks.getJSONObject(blockIndex);
            JSONArray lines = childBlock.getJSONArray("lines");
            if (lines == null) {
                continue;
            }
            for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
                JSONObject line = lines.getJSONObject(lineIndex);
                JSONArray spans = line.getJSONArray("spans");
                if (spans == null) {
                    continue;
                }
                for (int spanIndex = 0; spanIndex < spans.size(); spanIndex++) {
                    JSONObject span = spans.getJSONObject(spanIndex);
                    if (span.containsKey("content")) {
                        texts.add(span.getString("content"));
                    }
                }
            }
        }
        return joinList(texts);
    }

    private String extractImagePathFromMiddleBlock(JSONObject blockJson) {
        JSONArray blocks = blockJson.getJSONArray("blocks");
        if (blocks == null) {
            return null;
        }
        for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) {
            JSONObject childBlock = blocks.getJSONObject(blockIndex);
            JSONArray lines = childBlock.getJSONArray("lines");
            if (lines == null) {
                continue;
            }
            for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
                JSONObject line = lines.getJSONObject(lineIndex);
                JSONArray spans = line.getJSONArray("spans");
                if (spans == null) {
                    continue;
                }
                for (int spanIndex = 0; spanIndex < spans.size(); spanIndex++) {
                    JSONObject span = spans.getJSONObject(spanIndex);
                    if (span.containsKey("img_path")) {
                        return span.getString("img_path");
                    }
                }
            }
        }
        return null;
    }

    private List<String> extractTextsByType(JSONObject visualBlock, String expectedType) {
        List<String> texts = new ArrayList<String>();
        JSONArray blocks = visualBlock.getJSONArray("blocks");
        if (blocks == null) {
            return texts;
        }
        for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) {
            JSONObject childBlock = blocks.getJSONObject(blockIndex);
            if (!expectedType.equals(childBlock.getString("type"))) {
                continue;
            }
            JSONArray lines = childBlock.getJSONArray("lines");
            if (lines == null) {
                continue;
            }
            for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
                JSONObject line = lines.getJSONObject(lineIndex);
                JSONArray spans = line.getJSONArray("spans");
                if (spans == null) {
                    continue;
                }
                for (int spanIndex = 0; spanIndex < spans.size(); spanIndex++) {
                    JSONObject span = spans.getJSONObject(spanIndex);
                    if (span.containsKey("content")) {
                        texts.add(span.getString("content"));
                    }
                }
            }
        }
        return texts;
    }

    private String extractImagePathByType(JSONObject visualBlock, String expectedType) {
        JSONArray blocks = visualBlock.getJSONArray("blocks");
        if (blocks == null) {
            return null;
        }
        for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) {
            JSONObject childBlock = blocks.getJSONObject(blockIndex);
            if (!expectedType.equals(childBlock.getString("type"))) {
                continue;
            }
            JSONArray lines = childBlock.getJSONArray("lines");
            if (lines == null) {
                continue;
            }
            for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
                JSONObject line = lines.getJSONObject(lineIndex);
                JSONArray spans = line.getJSONArray("spans");
                if (spans == null) {
                    continue;
                }
                for (int spanIndex = 0; spanIndex < spans.size(); spanIndex++) {
                    JSONObject span = spans.getJSONObject(spanIndex);
                    if (span.containsKey("img_path")) {
                        return span.getString("img_path");
                    }
                }
            }
        }
        return null;
    }

    private String matchDataUrl(String imagePath, Map<String, String> imageDataUrls) {
        if (imageDataUrls == null || imageDataUrls.isEmpty()) {
            return null;
        }
        if (StringUtil.hasText(imagePath) && imageDataUrls.containsKey(imagePath)) {
            return imageDataUrls.get(imagePath);
        }
        String baseName = baseName(imagePath);
        if (!StringUtil.hasText(baseName)) {
            return null;
        }
        for (Map.Entry<String, String> entry : imageDataUrls.entrySet()) {
            if (baseName.equals(baseName(entry.getKey()))) {
                return entry.getValue();
            }
        }
        return null;
    }

    private byte[] matchBinaryContent(String imagePath, Map<String, byte[]> imageContents) {
        if (imageContents == null || imageContents.isEmpty()) {
            return null;
        }
        if (StringUtil.hasText(imagePath) && imageContents.containsKey(imagePath)) {
            return imageContents.get(imagePath);
        }
        String currentBaseName = baseName(imagePath);
        if (!StringUtil.hasText(currentBaseName)) {
            return null;
        }
        for (Map.Entry<String, byte[]> entry : imageContents.entrySet()) {
            if (currentBaseName.equals(baseName(entry.getKey()))) {
                return entry.getValue();
            }
        }
        return null;
    }

    private String baseName(String path) {
        if (!StringUtil.hasText(path)) {
            return null;
        }
        int slashIndex = path.lastIndexOf('/');
        return slashIndex >= 0 ? path.substring(slashIndex + 1) : path;
    }

    private String detectMimeType(String path) {
        if (!StringUtil.hasText(path)) {
            return null;
        }
        String mimeType = URLConnection.guessContentTypeFromName(path);
        return StringUtil.hasText(mimeType) ? mimeType : "application/octet-stream";
    }

    private String toDataUrl(String path, byte[] content) {
        return "data:" + detectMimeType(path) + ";base64," + Base64.getEncoder().encodeToString(content);
    }

    private byte[] decodeDataUrl(String dataUrl) {
        if (!StringUtil.hasText(dataUrl)) {
            return null;
        }
        int commaIndex = dataUrl.indexOf(',');
        if (commaIndex < 0 || commaIndex == dataUrl.length() - 1) {
            return null;
        }
        try {
            return Base64.getDecoder().decode(dataUrl.substring(commaIndex + 1));
        } catch (IllegalArgumentException exception) {
            return null;
        }
    }

    private String joinList(List<String> values) {
        if (values == null || values.isEmpty()) {
            return null;
        }
        StringBuilder builder = new StringBuilder();
        for (int index = 0; index < values.size(); index++) {
            if (index > 0) {
                builder.append('\n');
            }
            builder.append(values.get(index));
        }
        return builder.toString();
    }

    private boolean boolOrDefault(Boolean value, Boolean defaultValue) {
        return value == null ? isTrue(defaultValue) : value;
    }

    private boolean isTrue(Boolean value) {
        return value != null && value;
    }

    private int intOrDefault(Integer value, int defaultValue) {
        return value == null ? defaultValue : value;
    }

    private static class ZipArtifactBundle {
        private final Map<String, byte[]> entriesBySuffix = new LinkedHashMap<String, byte[]>();
        private final Map<String, byte[]> images = new LinkedHashMap<String, byte[]>();
        private final Map<String, byte[]> otherBinaryEntries = new LinkedHashMap<String, byte[]>();
    }
}
