package com.writeoff.module.ocr.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.writeoff.module.file.service.OssService;
import com.writeoff.module.ocr.config.BaiduOcrProperties;
import com.writeoff.module.ocr.dto.DocumentExtractTaskQueryResponse;
import com.writeoff.module.ocr.dto.DocumentExtractTaskSubmitRequest;
import com.writeoff.module.ocr.dto.DocumentExtractTaskSubmitResponse;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Service
public class BaiduDocumentExtractService {
    private static final long RESPONSE_MAX_BYTES = 8L * 1024 * 1024;

    private final BaiduOcrProperties props;
    private final BaiduOcrTokenService tokenService;
    private final OssService ossService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public BaiduDocumentExtractService(
            BaiduOcrProperties props,
            BaiduOcrTokenService tokenService,
            OssService ossService
    ) {
        this.props = props;
        this.tokenService = tokenService;
        this.ossService = ossService;
    }

    public DocumentExtractTaskSubmitResponse submitTask(DocumentExtractTaskSubmitRequest request) {
        validateSubmitRequest(request);

        String objectKey = trimToNull(request.getObjectKey());
        List<String> fileUrls = sanitizeFileUrls(request.getFileUrls());

        List<String> formParts = new ArrayList<String>();
        if (objectKey != null) {
            byte[] bytes = downloadFromOss(objectKey);
            String resolvedFileName = resolveFileName(request.getFileName(), objectKey);
            formParts.add(formParam("file", Base64.getEncoder().encodeToString(bytes)));
            formParts.add(formParam("fileName", resolvedFileName));
        } else {
            formParts.add(formParam("fileURLs", toJson(fileUrls)));
        }

        String manifestVersionId = trimToNull(request.getManifestVersionId());
        if (manifestVersionId != null) {
            formParts.add(formParam("manifestVersionId", manifestVersionId));
        } else {
            formParts.add(formParam("manifest", toJson(buildManifestPayload(request.getManifest()))));
        }

        if (request.getRemoveDuplicates() != null) {
            formParts.add(formParam("removeDuplicates", String.valueOf(request.getRemoveDuplicates())));
        }
        if (trimToNull(request.getPageRange()) != null) {
            formParts.add(formParam("pageRange", request.getPageRange().trim()));
        }
        if (request.getExtractSeal() != null) {
            formParts.add(formParam("extractSeal", String.valueOf(request.getExtractSeal())));
        }
        if (request.getEraseWatermark() != null) {
            formParts.add(formParam("eraseWatermark", String.valueOf(request.getEraseWatermark())));
        }

        JsonNode json = doPostForm(
                props.getDocumentExtractTaskUrl(),
                joinFormParts(formParts),
                "Baidu document extract submit failed"
        );

        DocumentExtractTaskSubmitResponse response = new DocumentExtractTaskSubmitResponse();
        response.setTaskId(asText(json.path("result").path("taskId")));
        response.setLogId(asText(json.path("log_id")));
        response.setRaw(toMap(json));
        return response;
    }

    public DocumentExtractTaskQueryResponse queryTask(String taskId) {
        String normalizedTaskId = trimToNull(taskId);
        if (normalizedTaskId == null) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "taskId must not be blank");
        }

        JsonNode json = doPostForm(
                props.getDocumentExtractQueryUrl(),
                formParam("taskId", normalizedTaskId),
                "Baidu document extract query failed"
        );

        JsonNode result = json.path("result");
        DocumentExtractTaskQueryResponse response = new DocumentExtractTaskQueryResponse();
        response.setTaskId(asText(result.path("taskId")));
        response.setStatus(asText(result.path("status")));
        response.setReason(asText(result.path("reason")));
        response.setCreatedAt(asText(result.path("createdAt")));
        response.setStartedAt(asText(result.path("startedAt")));
        response.setFinishedAt(asText(result.path("finishedAt")));
        response.setDuration(asLong(result.path("duration")));
        response.setLogId(asText(json.path("log_id")));
        response.setRaw(toMap(json));
        return response;
    }

    private void validateSubmitRequest(DocumentExtractTaskSubmitRequest request) {
        String objectKey = trimToNull(request.getObjectKey());
        List<String> fileUrls = sanitizeFileUrls(request.getFileUrls());
        if (objectKey == null && fileUrls.isEmpty()) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "objectKey or fileUrls is required");
        }
        if (objectKey != null && !fileUrls.isEmpty()) {
            fileUrls.clear();
        }

        String manifestVersionId = trimToNull(request.getManifestVersionId());
        if (manifestVersionId == null) {
            List<DocumentExtractTaskSubmitRequest.ManifestField> manifest = request.getManifest();
            if (manifest == null || manifest.isEmpty()) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "manifestVersionId or manifest is required");
            }
        }
    }

    private List<Map<String, String>> buildManifestPayload(List<DocumentExtractTaskSubmitRequest.ManifestField> manifest) {
        List<Map<String, String>> payload = new ArrayList<Map<String, String>>();
        if (manifest == null) {
            return payload;
        }
        for (DocumentExtractTaskSubmitRequest.ManifestField field : manifest) {
            if (field == null) {
                continue;
            }
            String key = trimToNull(field.getKey());
            if (key == null) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "manifest key must not be blank");
            }
            Map<String, String> item = new LinkedHashMap<String, String>();
            item.put("key", key);
            item.put("parentKey", defaultString(field.getParentKey()));
            item.put("description", defaultString(field.getDescription()));
            payload.add(item);
        }
        return payload;
    }

    private JsonNode doPostForm(String apiUrl, String formBody, String failurePrefix) {
        String endpoint = trimToNull(apiUrl);
        if (endpoint == null) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, failurePrefix + ": endpoint is not configured");
        }

        try {
            String accessToken = tokenService.getAccessToken();
            String requestUrl = endpoint + "?access_token=" + URLEncoder.encode(accessToken, "UTF-8");
            HttpURLConnection connection = (HttpURLConnection) new URL(requestUrl).openConnection();
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);
            connection.setConnectTimeout(Math.max(1000, props.getConnectTimeoutMs()));
            connection.setReadTimeout(Math.max(1000, props.getReadTimeoutMs()));
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

            try (OutputStream os = connection.getOutputStream()) {
                os.write(formBody.getBytes(StandardCharsets.UTF_8));
            }

            int code = connection.getResponseCode();
            InputStream stream = code >= 200 && code < 300 ? connection.getInputStream() : connection.getErrorStream();
            String text = new String(readAllBytesWithLimit(stream, RESPONSE_MAX_BYTES), StandardCharsets.UTF_8);
            JsonNode json = objectMapper.readTree(text == null ? "" : text);

            if (json.has("error_code") && json.path("error_code").asInt() != 0) {
                String errorMsg = asText(json.path("error_msg"));
                String logId = asText(json.path("log_id"));
                throw new BusinessException(
                        ErrorCodes.INTERNAL_ERROR,
                        failurePrefix + ": " + defaultString(errorMsg) + (logId == null ? "" : " (log_id=" + logId + ")")
                );
            }
            if (code < 200 || code >= 300) {
                throw new BusinessException(ErrorCodes.INTERNAL_ERROR, failurePrefix + ": HTTP " + code);
            }
            return json;
        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, failurePrefix + ": " + ex.getMessage());
        }
    }

    private byte[] downloadFromOss(String objectKey) {
        String signedUrl = ossService.generateDownloadUrl(objectKey);
        if (trimToNull(signedUrl) == null) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "Failed to generate OSS download url");
        }
        try {
            HttpURLConnection connection = (HttpURLConnection) new URL(signedUrl).openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(Math.max(1000, props.getConnectTimeoutMs()));
            connection.setReadTimeout(Math.max(1000, props.getReadTimeoutMs()));
            int code = connection.getResponseCode();
            InputStream stream = code >= 200 && code < 300 ? connection.getInputStream() : connection.getErrorStream();
            byte[] bytes = readAllBytesWithLimit(stream, props.getDocumentExtractMaxBytes());
            if (code < 200 || code >= 300) {
                throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "Failed to download OSS file: HTTP " + code);
            }
            if (bytes.length == 0) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "file content is empty");
            }
            return bytes;
        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "Failed to download OSS file: " + ex.getMessage());
        }
    }

    private String resolveFileName(String requestedFileName, String objectKey) {
        String fileName = trimToNull(requestedFileName);
        if (fileName != null) {
            return fileName;
        }
        String inferred = inferFileNameFromObjectKey(objectKey);
        if (inferred == null) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "fileName is required when objectKey has no filename");
        }
        return inferred;
    }

    private String inferFileNameFromObjectKey(String objectKey) {
        String text = trimToNull(objectKey);
        if (text == null) {
            return null;
        }
        int slash = Math.max(text.lastIndexOf('/'), text.lastIndexOf('\\'));
        String fileName = slash >= 0 ? text.substring(slash + 1) : text;
        return trimToNull(fileName);
    }

    private List<String> sanitizeFileUrls(List<String> fileUrls) {
        List<String> result = new ArrayList<String>();
        if (fileUrls == null) {
            return result;
        }
        for (String fileUrl : fileUrls) {
            String value = trimToNull(fileUrl);
            if (value != null) {
                result.add(value);
            }
        }
        return result;
    }

    private String toJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "Failed to serialize request body: " + ex.getMessage());
        }
    }

    private Map<String, Object> toMap(JsonNode json) {
        return objectMapper.convertValue(json, new TypeReference<Map<String, Object>>() {
        });
    }

    private String joinFormParts(List<String> formParts) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < formParts.size(); i++) {
            if (i > 0) {
                builder.append('&');
            }
            builder.append(formParts.get(i));
        }
        return builder.toString();
    }

    private String formParam(String key, String value) {
        try {
            return URLEncoder.encode(key, "UTF-8") + "=" + URLEncoder.encode(value == null ? "" : value, "UTF-8");
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "Failed to encode request body: " + ex.getMessage());
        }
    }

    private static String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String text = value.trim();
        return text.isEmpty() ? null : text;
    }

    private static String defaultString(String value) {
        return value == null ? "" : value.trim();
    }

    private static String asText(JsonNode node) {
        if (node == null || node.isMissingNode() || node.isNull()) {
            return null;
        }
        String text = node.asText();
        if (text == null) {
            return null;
        }
        text = text.trim();
        return text.isEmpty() ? null : text;
    }

    private static Long asLong(JsonNode node) {
        if (node == null || node.isMissingNode() || node.isNull()) {
            return null;
        }
        if (node.isNumber()) {
            return node.asLong();
        }
        String text = asText(node);
        if (text == null) {
            return null;
        }
        try {
            return Long.parseLong(text);
        } catch (Exception ex) {
            return null;
        }
    }

    private static byte[] readAllBytesWithLimit(InputStream is, long maxBytes) throws java.io.IOException {
        if (is == null) {
            return new byte[0];
        }
        long limit = maxBytes <= 0 ? 0 : maxBytes;
        try (InputStream in = is; ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[8192];
            long total = 0;
            int n;
            while ((n = in.read(buffer)) >= 0) {
                if (n == 0) {
                    continue;
                }
                total += n;
                if (limit > 0 && total > limit) {
                    throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "file exceeds size limit");
                }
                bos.write(buffer, 0, n);
            }
            return bos.toByteArray();
        }
    }
}
