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.MultipleInvoiceOcrResponse;
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.Base64;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

@Service
public class BaiduMultipleInvoiceOcrService {
    private final BaiduOcrProperties props;
    private final BaiduOcrTokenService tokenService;
    private final OssService ossService;
    private final ObjectMapper objectMapper = new ObjectMapper();

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

    public MultipleInvoiceOcrResponse recognize(String objectKey) {
        String key = String.valueOf(objectKey == null ? "" : objectKey).trim();
        if (key.isEmpty()) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "objectKey不能为空");
        }

        byte[] bytes = downloadFromOss(key);
        if (bytes.length <= 0) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "文件内容为空");
        }

        Map<String, Object> raw = callBaiduMultipleInvoice(key, bytes);
        MultipleInvoiceOcrResponse resp = new MultipleInvoiceOcrResponse();
        resp.setRaw(raw);

        NormalizedPick pick = pickBestResult(raw);
        if (pick != null) {
            resp.setDetectedType(pick.type);
            resp.setProbability(pick.probability);
            resp.setNormalized(normalize(pick.result, pick.type));
        } else {
            resp.setDetectedType(null);
            resp.setProbability(null);
            resp.setNormalized(new MultipleInvoiceOcrResponse.Normalized());
        }
        return resp;
    }

    private byte[] downloadFromOss(String objectKey) {
        String signedUrl = ossService.generateDownloadUrl(objectKey);
        if (signedUrl == null || signedUrl.trim().isEmpty()) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "生成OSS下载链接失败");
        }
        try {
            URL url = new URL(signedUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(Math.max(1000, props.getConnectTimeoutMs()));
            conn.setReadTimeout(Math.max(1000, props.getReadTimeoutMs()));
            int status = conn.getResponseCode();
            InputStream is = status >= 200 && status < 300 ? conn.getInputStream() : conn.getErrorStream();
            byte[] data = readAllBytesWithLimit(is, props.getMaxBytes());
            if (status < 200 || status >= 300) {
                throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "拉取OSS文件失败: HTTP " + status);
            }
            return data;
        } catch (BusinessException be) {
            throw be;
        } catch (Exception e) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "拉取OSS文件异常: " + e.getMessage());
        }
    }

    private Map<String, Object> callBaiduMultipleInvoice(String objectKey, byte[] bytes) {
        String accessToken = tokenService.getAccessToken();
        String apiUrl = props.getMultipleInvoiceUrl();
        if (apiUrl == null || apiUrl.trim().isEmpty()) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "百度OCR multipleInvoiceUrl未配置");
        }

        boolean isPdf = objectKey.toLowerCase().endsWith(".pdf");
        String fieldName = isPdf ? "pdf_file" : "image";
        String b64 = Base64.getEncoder().encodeToString(bytes);

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

            try (OutputStream os = conn.getOutputStream()) {
                os.write(form.getBytes(StandardCharsets.UTF_8));
            }

            int status = conn.getResponseCode();
            InputStream is = status >= 200 && status < 300 ? conn.getInputStream() : conn.getErrorStream();
            String body = new String(readAllBytesWithLimit(is, 2 * 1024 * 1024), StandardCharsets.UTF_8);
            JsonNode json = objectMapper.readTree(body == null ? "" : body);

            if (json.has("error_code")) {
                String errMsg = json.has("error_msg") ? json.get("error_msg").asText() : "unknown";
                String logId = json.has("log_id") ? json.get("log_id").asText() : "";
                throw new BusinessException(
                    ErrorCodes.INTERNAL_ERROR,
                    "百度OCR识别失败: " + errMsg + (logId.isEmpty() ? "" : " (log_id=" + logId + ")")
                );
            }
            if (status < 200 || status >= 300) {
                throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "百度OCR识别失败: HTTP " + status);
            }
            return objectMapper.convertValue(json, new TypeReference<Map<String, Object>>() {
            });
        } catch (BusinessException be) {
            throw be;
        } catch (Exception e) {
            throw new BusinessException(ErrorCodes.INTERNAL_ERROR, "百度OCR调用异常: " + e.getMessage());
        }
    }

    private static class NormalizedPick {
        final String type;
        final Double probability;
        final Map<String, Object> result;

        NormalizedPick(String type, Double probability, Map<String, Object> result) {
            this.type = type;
            this.probability = probability;
            this.result = result;
        }
    }

    @SuppressWarnings("unchecked")
    private NormalizedPick pickBestResult(Map<String, Object> raw) {
        Object wordsResult = raw == null ? null : raw.get("words_result");
        if (!(wordsResult instanceof Iterable)) {
            return null;
        }
        return ((Iterable<?>) wordsResult).iterator().hasNext() ? streamPick((Iterable<?>) wordsResult) : null;
    }

    @SuppressWarnings("unchecked")
    private NormalizedPick streamPick(Iterable<?> items) {
        return java.util.stream.StreamSupport.stream(items.spliterator(), false)
            .filter(x -> x instanceof Map)
            .map(x -> (Map<String, Object>) x)
            .map(map -> {
                String type = firstNonBlank(
                    trimToNull(map.get("type")),
                    trimToNull(map.get("detectedType"))
                );
                Double prob = null;
                Object p = map.get("probability");
                if (p instanceof Number) {
                    prob = ((Number) p).doubleValue();
                } else if (p != null) {
                    try {
                        prob = Double.parseDouble(String.valueOf(p));
                    } catch (Exception ignored) {
                        prob = null;
                    }
                }
                Object res = map.get("result");
                Map<String, Object> result = res instanceof Map ? (Map<String, Object>) res : new LinkedHashMap<>();
                return new NormalizedPick(normalizeType(type), prob, result);
            })
            .max(Comparator.comparingDouble(p -> p.probability == null ? -1.0 : p.probability))
            .orElse(null);
    }

    private MultipleInvoiceOcrResponse.Normalized normalize(Map<String, Object> result, String type) {
        MultipleInvoiceOcrResponse.Normalized normalized = new MultipleInvoiceOcrResponse.Normalized();
        if (result == null) {
            return normalized;
        }

        String normalizedType = normalizeType(type);
        if ("vat_invoice".equals(normalizedType)) {
            normalizeVatInvoice(normalized, result);
        } else if ("air_ticket".equals(normalizedType)) {
            normalizeAirTicket(normalized, result);
        }

        fillGenericFallback(normalized, result);
        return normalized;
    }

    private void normalizeVatInvoice(MultipleInvoiceOcrResponse.Normalized normalized, Map<String, Object> result) {
        normalized.setInvoiceCode(extractFirstWord(
            result,
            "InvoiceCode",
            "InvoiceCodeConfirm",
            "invoice_code",
            "fapiao-daima"
        ));
        normalized.setInvoiceNum(extractFirstWord(
            result,
            "InvoiceNum",
            "InvoiceNumConfirm",
            "invoice_num",
            "fapiao-haoma",
            "invoice_number"
        ));
        normalized.setName(extractFirstWord(
            result,
            "SellerName",
            "seller_name",
            "name"
        ));
        normalized.setTaxCent(parseMoneyToCent(extractFirstWord(
            result,
            "TotalTax",
            "TotalTaxAmount",
            "tax_total",
            "tax"
        )));
        normalized.setTotalAmountCent(parseMoneyToCent(extractFirstWord(
            result,
            "AmountInFiguers",
            "AmountInFigures",
            "amount_in_figuers",
            "amount_in_figures",
            "price-tax-small",
            "small_price",
            "price_tax_small"
        )));
    }

    private void normalizeAirTicket(MultipleInvoiceOcrResponse.Normalized normalized, Map<String, Object> result) {
        normalized.setInvoiceCode(extractFirstWord(
            result,
            "invoice_code",
            "InvoiceCode",
            "fapiao-daima"
        ));
        normalized.setInvoiceNum(extractFirstWord(
            result,
            "invoice_num",
            "InvoiceNum",
            "invoice_number",
            "ticket_num"
        ));
        normalized.setName(extractFirstWord(
            result,
            "name",
            "Name",
            "PassengName",
            "PassengerName"
        ));
        normalized.setTaxCent(parseMoneyToCent(extractFirstWord(
            result,
            "commodity_tax",
            "CommodityTax",
            "tax_total",
            "tax"
        )));
        normalized.setTotalAmountCent(parseMoneyToCent(extractFirstWord(
            result,
            "ticket_rates",
            "ticket_rates_in_figures",
            "invoice_rate",
            "invoice_rate_in_figure",
            "TotalFare",
            "total_fare",
            "Fare",
            "fare"
        )));
    }

    private void fillGenericFallback(MultipleInvoiceOcrResponse.Normalized normalized, Map<String, Object> result) {
        if (isBlank(normalized.getInvoiceCode())) {
            normalized.setInvoiceCode(extractFirstWord(
                result,
                "InvoiceCode",
                "InvoiceCodeConfirm",
                "invoice_code",
                "fapiao-daima"
            ));
        }
        if (isBlank(normalized.getInvoiceNum())) {
            normalized.setInvoiceNum(extractFirstWord(
                result,
                "InvoiceNum",
                "InvoiceNumConfirm",
                "invoice_num",
                "fapiao-haoma",
                "invoice_number",
                "ticket_num"
            ));
        }
        if (isBlank(normalized.getName())) {
            normalized.setName(extractFirstWord(
                result,
                "SellerName",
                "seller_name",
                "PassengName",
                "PassengerName",
                "name",
                "Name",
                "buyer-name"
            ));
        }
        if (normalized.getTaxCent() == null) {
            normalized.setTaxCent(parseMoneyToCent(extractFirstWord(
                result,
                "TotalTax",
                "commodity_tax",
                "CommodityTax",
                "tax",
                "TotalTaxAmount",
                "tax_total"
            )));
        }
        if (normalized.getTotalAmountCent() == null) {
            normalized.setTotalAmountCent(parseMoneyToCent(extractFirstWord(
                result,
                "ticket_rates",
                "AmountInFiguers",
                "AmountInFigures",
                "amount_in_figuers",
                "amount_in_figures",
                "TotalAmount",
                "total_amount",
                "TotalFare",
                "total_fare",
                "Fare",
                "fare",
                "ticket_rates_in_figures",
                "invoice_rate",
                "invoice_rate_in_figure",
                "price-tax-small",
                "small_price",
                "price_tax_small"
            )));
        }
    }

    @SuppressWarnings("unchecked")
    private static String extractFirstWord(Map<String, Object> result, String... keys) {
        for (String key : keys) {
            Object val = result.get(key);
            if (val == null) {
                continue;
            }
            if (val instanceof Iterable) {
                for (Object item : (Iterable<?>) val) {
                    if (item instanceof Map) {
                        Object word = ((Map<String, Object>) item).get("word");
                        String text = String.valueOf(word == null ? "" : word).trim();
                        if (!text.isEmpty()) {
                            return text;
                        }
                    }
                }
            } else if (val instanceof Map) {
                Object word = ((Map<String, Object>) val).get("word");
                String text = String.valueOf(word == null ? "" : word).trim();
                if (!text.isEmpty()) {
                    return text;
                }
            } else {
                String text = String.valueOf(val).trim();
                if (!text.isEmpty()) {
                    return text;
                }
            }
        }
        return null;
    }

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

    private static String firstNonBlank(String... values) {
        for (String value : values) {
            if (value != null && !value.trim().isEmpty()) {
                return value.trim();
            }
        }
        return null;
    }

    private static String normalizeType(String type) {
        return type == null ? null : type.trim().toLowerCase(Locale.ROOT);
    }

    private static boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }

    private static Long parseMoneyToCent(String raw) {
        if (raw == null) {
            return null;
        }
        String s = raw.trim();
        if (s.isEmpty()) {
            return null;
        }
        s = s.replaceAll("[,，\\s]", "");
        String cleaned = s.replaceAll("[^0-9.\\-]", "");
        if (cleaned.isEmpty() || cleaned.equals("-") || cleaned.equals(".")) {
            return null;
        }
        try {
            java.math.BigDecimal bd = new java.math.BigDecimal(cleaned);
            bd = bd.setScale(2, java.math.RoundingMode.HALF_UP);
            return bd.multiply(new java.math.BigDecimal("100")).longValueExact();
        } catch (Exception e) {
            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[] buf = new byte[8192];
            long total = 0;
            int n;
            while ((n = in.read(buf)) >= 0) {
                if (n == 0) {
                    continue;
                }
                total += n;
                if (limit > 0 && total > limit) {
                    throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "文件过大，超过限制");
                }
                bos.write(buf, 0, n);
            }
            return bos.toByteArray();
        }
    }
}
