package com.easyagents.document.core.mineru;

import com.alibaba.fastjson2.JSONObject;
import com.easyagents.core.util.StringUtil;
import com.easyagents.document.core.exception.DocumentParseException;
import com.easyagents.document.core.entity.ParseFile;
import com.easyagents.document.core.entity.ParseRequest;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

import java.io.IOException;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * MinerU HTTP 客户端。
 *
 * @author Codex
 * @since 2026-04-16
 */
public class MineruClient {

    private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.parse("application/octet-stream");
    private static final int DEFAULT_SUBMIT_TIMEOUT_MS = 120000;
    private static final int AVAILABILITY_PROBE_TIMEOUT_MS = 3000;
    private static final long AVAILABILITY_CACHE_TTL_MS = 5000L;

    private final String baseUrl;
    private final OkHttpClient okHttpClient;
    private final MineruMapper mineruMapper;
    private final int submitTimeoutMs;
    private final Object availabilityProbeMonitor = new Object();
    private volatile long availabilityCacheDeadlineMs;
    private volatile String availabilityFailureMessage;

    /**
     * 创建客户端。
     *
     * @param properties MinerU 配置
     * @param mineruMapper DTO 映射器
     */
    public MineruClient(MineruProperties properties, MineruMapper mineruMapper) {
        this(
            properties,
            new OkHttpClient.Builder()
                .connectTimeout(properties.getConnectTimeoutMs(), TimeUnit.MILLISECONDS)
                .readTimeout(properties.getReadTimeoutMs(), TimeUnit.MILLISECONDS)
                .writeTimeout(properties.getWriteTimeoutMs(), TimeUnit.MILLISECONDS)
                .build(),
            mineruMapper
        );
    }

    /**
     * 创建客户端。
     *
     * @param properties MinerU 配置
     * @param okHttpClient HTTP 客户端
     * @param mineruMapper DTO 映射器
     */
    public MineruClient(MineruProperties properties, OkHttpClient okHttpClient, MineruMapper mineruMapper) {
        if (properties == null || !StringUtil.hasText(properties.getBaseUrl())) {
            throw new IllegalArgumentException("MinerU baseUrl must not be empty");
        }
        this.baseUrl = normalizeBaseUrl(properties.getBaseUrl());
        this.okHttpClient = okHttpClient;
        this.mineruMapper = mineruMapper;
        this.submitTimeoutMs = positiveOrDefault(properties.getSubmitTimeoutMs(), DEFAULT_SUBMIT_TIMEOUT_MS);
    }

    /**
     * 调用同步解析接口。
     *
     * @param request 解析请求
     * @return 原始结果
     */
    public MineruResultPayload parse(ParseRequest request) {
        return mineruMapper.toResultPayload(executeJsonMultipart("/file_parse", request, buildSyncFormFields(request)));
    }

    /**
     * 提交异步解析任务。
     *
     * @param request 解析请求
     * @return 原始任务状态
     */
    public MineruTaskStatus submit(ParseRequest request) {
        assertServiceAvailable();
        return mineruMapper.toTaskStatus(
            executeJsonMultipart("/tasks", request, buildAsyncFormFields(request), submitTimeoutMs)
        );
    }

    /**
     * 查询任务状态。
     *
     * @param taskId 任务 ID
     * @return 原始任务状态
     */
    public MineruTaskStatus queryTask(String taskId) {
        return mineruMapper.toTaskStatus(executeJsonGet("/tasks/" + taskId));
    }

    /**
     * 下载异步结果 ZIP。
     *
     * @param taskId 任务 ID
     * @return ZIP 二进制
     */
    public byte[] queryResultZip(String taskId) {
        String path = "/tasks/" + taskId + "/result";
        Request request = new Request.Builder().url(baseUrl + path).get().build();
        try (Response response = okHttpClient.newCall(request).execute()) {
            ResponseBody body = response.body();
            byte[] responseBytes = body == null ? new byte[0] : body.bytes();
            if (!response.isSuccessful()) {
                throw buildHttpException(path, response.code(), responseBytes);
            }
            String contentType = response.header("Content-Type");
            if (contentType != null && contentType.contains("application/json")) {
                JSONObject jsonObject = MineruJsonSupport.parseObject(
                    new String(responseBytes, StandardCharsets.UTF_8),
                    "MinerU async result response: " + path
                );
                throw new DocumentParseException("MinerU async result is not ready: " + jsonObject.toJSONString());
            }
            if (responseBytes.length < 2 || responseBytes[0] != 'P' || responseBytes[1] != 'K') {
                throw new DocumentParseException("MinerU async result is not a valid ZIP payload");
            }
            return responseBytes;
        } catch (IOException exception) {
            throw new DocumentParseException("Failed to query MinerU result ZIP", exception);
        }
    }

    protected JSONObject executeJsonMultipart(String path, ParseRequest request, Map<String, List<String>> fields) {
        return executeJsonMultipart(path, request, fields, 0);
    }

    /**
     * 执行带整次调用超时的 Multipart JSON 请求。
     *
     * @param path 接口路径
     * @param request 解析请求
     * @param fields 表单字段
     * @param callTimeoutMs 整次调用超时时间，单位毫秒；小于等于 0 时沿用客户端阶段超时
     * @return JSON 响应
     */
    protected JSONObject executeJsonMultipart(String path,
                                              ParseRequest request,
                                              Map<String, List<String>> fields,
                                              long callTimeoutMs) {
        MultipartBody.Builder formBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
        appendFiles(formBuilder, request.getFiles());
        appendStringFields(formBuilder, fields);
        Request httpRequest = new Request.Builder()
            .url(baseUrl + path)
            .post(formBuilder.build())
            .build();
        return callTimeoutMs > 0
            ? executeJsonRequest(path, httpRequest, callTimeoutMs)
            : executeJsonRequest(path, httpRequest);
    }

    protected JSONObject executeJsonGet(String path) {
        Request request = new Request.Builder().url(baseUrl + path).get().build();
        return executeJsonRequest(path, request);
    }

    protected JSONObject executeJsonRequest(String path, Request request) {
        return executeJsonRequest(path, request, 0);
    }

    /**
     * 执行 JSON 请求，并可限制连接、写入和读取在内的整次调用时长。
     *
     * @param path 接口路径
     * @param request HTTP 请求
     * @param callTimeoutMs 整次调用超时时间，单位毫秒；小于等于 0 时不额外限制
     * @return JSON 响应
     */
    protected JSONObject executeJsonRequest(String path, Request request, long callTimeoutMs) {
        Call call = okHttpClient.newCall(request);
        if (callTimeoutMs > 0) {
            // OkHttp 的 Call timeout 到期后会取消底层请求，避免线程长期阻塞在文件上传或响应等待。
            call.timeout().timeout(callTimeoutMs, TimeUnit.MILLISECONDS);
        }
        try (Response response = call.execute()) {
            if (response.code() >= 500) {
                // 服务端错误在响应头到达时立即失败，避免异常响应体未结束时继续占用提交线程。
                throw buildHttpException(path, response.code(), new byte[0]);
            }
            ResponseBody body = response.body();
            String bodyText = body == null ? "" : body.string();
            if (!response.isSuccessful()) {
                throw buildHttpException(
                    path,
                    response.code(),
                    bodyText == null ? new byte[0] : bodyText.getBytes(StandardCharsets.UTF_8)
                );
            }
            return MineruJsonSupport.parseObject(bodyText, "MinerU response body: " + path);
        } catch (IOException exception) {
            throw new DocumentParseException("Failed to call MinerU endpoint: " + path, exception);
        }
    }

    /**
     * 在上传文件前探测 MinerU 网关是否可用，避免服务已返回 5xx 时仍传输大文件。
     *
     * <p>探测结果短暂缓存，限制批量导入期间的额外请求量。健康检查返回 4xx 说明网关仍可达，
     * 实际任务接口会继续完成业务校验。</p>
     *
     * @throws DocumentParseException 网关返回 5xx 或探测请求失败
     */
    private void assertServiceAvailable() {
        long now = System.currentTimeMillis();
        if (now < availabilityCacheDeadlineMs) {
            throwCachedAvailabilityFailure();
            return;
        }
        synchronized (availabilityProbeMonitor) {
            now = System.currentTimeMillis();
            if (now < availabilityCacheDeadlineMs) {
                throwCachedAvailabilityFailure();
                return;
            }
            String healthPath = "/health";
            Request request = new Request.Builder().url(baseUrl + healthPath).get().build();
            Call call = okHttpClient.newCall(request);
            call.timeout().timeout(
                Math.min(submitTimeoutMs, AVAILABILITY_PROBE_TIMEOUT_MS),
                TimeUnit.MILLISECONDS
            );
            try (Response response = call.execute()) {
                if (response.code() >= 500) {
                    cacheAvailabilityFailure(
                        "MinerU service unavailable: path=" + healthPath + ", status=" + response.code(),
                        now
                    );
                    throw new DocumentParseException(availabilityFailureMessage);
                }
                availabilityFailureMessage = null;
                availabilityCacheDeadlineMs = now + AVAILABILITY_CACHE_TTL_MS;
            } catch (IOException exception) {
                cacheAvailabilityFailure("MinerU service unavailable: availability probe failed", now);
                throw new DocumentParseException(availabilityFailureMessage, exception);
            }
        }
    }

    /**
     * 缓存 MinerU 不可用状态。
     *
     * @param message 失败信息
     * @param detectedAtMs 检测时间，单位毫秒
     */
    private void cacheAvailabilityFailure(String message, long detectedAtMs) {
        availabilityFailureMessage = message;
        availabilityCacheDeadlineMs = detectedAtMs + AVAILABILITY_CACHE_TTL_MS;
    }

    /**
     * 若缓存记录为不可用则抛出稳定异常。
     *
     * @throws DocumentParseException MinerU 仍处于不可用缓存窗口
     */
    private void throwCachedAvailabilityFailure() {
        if (availabilityFailureMessage != null) {
            throw new DocumentParseException(availabilityFailureMessage);
        }
    }

    /**
     * 返回正整数配置，非法配置回退到缺省值。
     *
     * @param value 配置值
     * @param defaultValue 缺省值
     * @return 可用的正整数
     */
    private int positiveOrDefault(Integer value, int defaultValue) {
        return value == null || value <= 0 ? defaultValue : value;
    }

    private void appendFiles(MultipartBody.Builder formBuilder, List<ParseFile> files) {
        if (files == null || files.isEmpty()) {
            throw new IllegalArgumentException("Parse request must contain at least one file");
        }
        for (ParseFile file : files) {
            if (file == null || !StringUtil.hasText(file.getFileName()) || file.getContent() == null) {
                throw new IllegalArgumentException("Parse request contains an invalid file");
            }
            MediaType mediaType = StringUtil.hasText(file.getContentType())
                ? MediaType.parse(file.getContentType())
                : detectMediaType(file.getFileName());
            formBuilder.addFormDataPart(
                "files",
                file.getFileName(),
                RequestBody.create(file.getContent(), mediaType)
            );
        }
    }

    private void appendStringFields(MultipartBody.Builder formBuilder, Map<String, List<String>> fields) {
        for (Map.Entry<String, List<String>> entry : fields.entrySet()) {
            if (entry.getValue() == null) {
                continue;
            }
            for (String value : entry.getValue()) {
                if (value != null) {
                    formBuilder.addFormDataPart(entry.getKey(), value);
                }
            }
        }
    }

    private Map<String, List<String>> buildSyncFormFields(ParseRequest request) {
        return mineruMapper.buildSyncFormFields(request);
    }

    private Map<String, List<String>> buildAsyncFormFields(ParseRequest request) {
        return mineruMapper.buildAsyncFormFields(request);
    }

    private DocumentParseException buildHttpException(String path, int statusCode, byte[] bodyBytes) {
        String bodyText = bodyBytes == null ? "" : new String(bodyBytes, StandardCharsets.UTF_8);
        return new DocumentParseException(
            "MinerU request failed: path=" + path + ", status=" + statusCode + ", body=" + bodyText
        );
    }

    private String normalizeBaseUrl(String baseUrl) {
        if (baseUrl.endsWith("/")) {
            return baseUrl.substring(0, baseUrl.length() - 1);
        }
        return baseUrl;
    }

    private MediaType detectMediaType(String fileName) {
        String mimeType = URLConnection.guessContentTypeFromName(fileName);
        return StringUtil.hasText(mimeType) ? MediaType.parse(mimeType) : DEFAULT_MEDIA_TYPE;
    }
}
