package com.ruoyi.app.utils.ezPay;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.app.utils.ezPayCrypto.EzPayEncryptUtil;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * ezPay 电子发票加值服务平台 客户端。
 *
 * <p>串接流程：业务参数组装 → PostData_ 加密 → Form Post(UTF-8) → JSON 回应。
 * 对应官方手册 EZP_INVI_1.2.1（开立 / 作废 / 折让 / 查询）。
 *
 * <p>多商家：每个商家在 ezPay 申请独立的 HashKey / HashIV，调用时传入 {@link EzPayConfig}。
 *
 * <p>用法示例（即时开立 B2C 发票）：
 * <pre>{@code
 * EzPayConfig cfg = new EzPayConfig("3482911", "商店HashKey32字节", "商店HashIV16字节");
 * Map<String, Object> inv = new LinkedHashMap<>();
 * inv.put("MerchantOrderNo", "ORD20260615001");     // 商店自订单号（同商店唯一）
 * inv.put("Category", "B2C");                         // B2B=营业人 B2C=个人
 * inv.put("BuyerName", "王小明");
 * inv.put("PrintFlag", "Y");                          // 无载具/捐赠时必填 Y
 * inv.put("TaxType", "1");                            // 1=应税
 * inv.put("TaxRate", "5");
 * inv.put("Amt", "490");                              // 销售额(未税)
 * inv.put("TaxAmt", "10");                            // 税额
 * inv.put("TotalAmt", "500");                         // 含税金额 = 销售额+税额
 * inv.put("ItemName", "餐点A|餐点B");                 // 多品项用 | 分隔
 * inv.put("ItemCount", "1|2");
 * inv.put("ItemUnit", "個|個");
 * inv.put("ItemPrice", "300|100");                    // B2C 为含税单价
 * inv.put("ItemAmt", "300|200");
 * JSONObject resp = ezPay.issueInvoice(EzPay.BASE_TEST, cfg, inv);
 * // resp.result 含 InvoiceNumber / RandomNum / InvoiceTransNo ...
 * }</pre>
 *
 * @author ruoyi
 */
@Component
public class EzPay {

    private static final Logger log = LoggerFactory.getLogger(EzPay.class);

    /** 正式环境根地址。 */
    public static final String BASE_PROD = "https://inv.ezpay.com.tw";
    /** 测试环境根地址。 */
    public static final String BASE_TEST = "https://cinv.ezpay.com.tw";

    // ===== 发票接口（INVI）=====
    /** 开立发票 */
    public static final String URL_ISSUE = "/Api/invoice_issue";
    /** 触发开立（Status=0 等待触发 / Status=3 预约自动 时使用） */
    public static final String URL_TOUCH_ISSUE = "/Api/invoice_touch_issue";
    /** 作废发票 */
    public static final String URL_INVALID = "/Api/invoice_invalid";
    /** 开立折让 */
    public static final String URL_ALLOWANCE_ISSUE = "/Api/allowance_issue";
    /** 触发确认 / 取消折让 */
    public static final String URL_ALLOWANCE_TOUCH = "/Api/allowance_touch_issue";
    /** 作废折让 */
    public static final String URL_ALLOWANCE_INVALID = "/Api/allowanceInvalid";
    /** 查询发票 */
    public static final String URL_SEARCH = "/Api/invoice_search";

    // ===== 验证接口（BDV）=====
    /** 手机条码验证 */
    public static final String URL_CHECK_BARCODE = "/Api_inv_application/checkBarCode";
    /** 捐赠码验证 */
    public static final String URL_CHECK_LOVECODE = "/Api_inv_application/checkLoveCode";

    /**
     * 开立发票（即时开立 Status=1）。对应 POST /Api/invoice_issue（Version=1.5）。
     *
     * @param baseUrl {@link #BASE_PROD} 或 {@link #BASE_TEST}
     * @param config  商店配置
     * @param invoice 发票业务参数（<b>不要</b>带 RespondType / Version / TimeStamp / Status，
     *                由本方法自动补充；其余如 Category / BuyerName / Amt / TotalAmt / ItemName 等由调用方填）
     * @return 回应 JSON，含 Status / Message / Result，Result 内有 InvoiceNumber / RandomNum 等
     */
    public JSONObject issueInvoice(String baseUrl, EzPayConfig config, Map<String, Object> invoice) throws Exception {
        Map<String, Object> postData = new LinkedHashMap<>(invoice);
        postData.put("RespondType", "JSON");
        postData.put("Version", "1.5");
        postData.put("Status", "1"); // 1=即时开立
        return doPost(baseUrl + URL_ISSUE, config, postData);
    }

    /**
     * 查询发票（按发票号 + 防伪随机码）。对应 POST /Api/invoice_search（Version=1.3）。
     */
    public JSONObject searchInvoice(String baseUrl, EzPayConfig config,
                                    String invoiceNumber, String randomNum) throws Exception {
        Map<String, Object> postData = new LinkedHashMap<>();
        postData.put("RespondType", "JSON");
        postData.put("Version", "1.3");
        postData.put("SearchType", "0"); // 0=发票号+随机码；1=订单号+发票金额
        postData.put("InvoiceNumber", invoiceNumber);
        postData.put("RandomNum", randomNum);
        return doPost(baseUrl + URL_SEARCH, config, postData);
    }

    /**
     * 捐赠码验证。对应 POST /Api_inv_application/checkLoveCode（BDV v1.0）。
     *
     * <p>BDV 请求需带 CheckValue（与发票类接口 issue/invalid/search 不同，那些只发 MerchantID_+PostData_）。
     * 回应 Result 为加密 hex，解密后形如 {@code Lovecode=919&IsExist=Y}。
     *
     * <p><b>文档勘误</b>：CheckValue 按手册「HashKey 在前」实现（{@link EzPayEncryptUtil#genCheckValue}），
     * 若测试环境 ezPay 回 CheckValue 校验失败，改为「HashIV 在前」重试（见加密工具 javadoc）。
     *
     * @return true=捐赠码存在于财政部电子发票整合服务平台
     */
    public boolean checkLoveCode(String baseUrl, EzPayConfig config, String loveCode) throws Exception {
        // PostData_ 内含字段：仅 TimeStamp + LoveCode（Version/RespondType 是顶层表单字段，见 BDV 手册五-(一)）
        Map<String, Object> postData = new LinkedHashMap<>();
        postData.put("TimeStamp", String.valueOf(System.currentTimeMillis() / 1000L));
        postData.put("LoveCode", loveCode);
        String query = buildQuery(postData);
        String postDataHex = EzPayEncryptUtil.encrypt(query, config.getHashKey(), config.getHashIV());
        String checkValue = EzPayEncryptUtil.genCheckValue(postDataHex, config.getHashKey(), config.getHashIV());
        String resp = postFormWithCheck(baseUrl + URL_CHECK_LOVECODE, config.getMerchantId(), "1.0", "JSON", postDataHex, checkValue);
        JSONObject json = JSON.parseObject(resp);
        if (json == null || !"SUCCESS".equals(json.getString("Status"))) {
            return false;
        }
        String resultHex = json.getString("Result");
        if (resultHex == null || resultHex.isEmpty()) {
            return false;
        }
        String decrypted = EzPayEncryptUtil.decrypt(resultHex, config.getHashKey(), config.getHashIV());
        // decrypted 形如 Lovecode=919&IsExist=Y
        return decrypted != null && decrypted.matches(".*IsExist=Y(\\b|$).*");
    }

    /**
     * 手机条码验证。对应 POST /Api_inv_application/checkBarCode（BDV v1.0）。
     *
     * <p>校验手机条码是否真实注册于财政部电子发票整合服务平台（通用载具），用于结账时拦截无效条码、
     * 避免发票存进不存在的载具（客户填错条码 → 发票丢失）。与 {@link #checkLoveCode} 同为 BDV 接口，
     * 请求需带 CheckValue，回应 Result 为加密 hex，解密后形如 {@code CellphoneBarcode=%2FABC1234&IsExist=Y}。
     *
     * <p><b>文档勘误</b>：CheckValue 按手册「HashKey 在前」实现（{@link EzPayEncryptUtil#genCheckValue}），
     * 若测试环境 ezPay 回 CheckValue 校验失败，改为「HashIV 在前」重试（见加密工具 javadoc）。
     *
     * @param barCode 手机条码（/ + 7 位大写英文/数字/+-.）
     * @return true=该手机条码存在于财政部平台
     */
    public boolean checkBarCode(String baseUrl, EzPayConfig config, String barCode) throws Exception {
        // PostData_ 内含字段：仅 TimeStamp + CellphoneBarcode（Version/RespondType 是顶层表单字段，见 BDV 手册四-(一)）
        Map<String, Object> postData = new LinkedHashMap<>();
        postData.put("TimeStamp", String.valueOf(System.currentTimeMillis() / 1000L));
        postData.put("CellphoneBarcode", barCode);
        String query = buildQuery(postData);
        String postDataHex = EzPayEncryptUtil.encrypt(query, config.getHashKey(), config.getHashIV());
        String checkValue = EzPayEncryptUtil.genCheckValue(postDataHex, config.getHashKey(), config.getHashIV());
        log.info("[EzPay] checkBarCode 全量参数 >>> MerchantID={}, Version=1.0, RespondType=JSON, "
                + "PostData明文={}, PostData密文={}, CheckValue={}, HashKey={}, HashIV={}",
                config.getMerchantId(), postData, postDataHex, checkValue, config.getHashKey(), config.getHashIV());
        String resp = postFormWithCheck(baseUrl + URL_CHECK_BARCODE, config.getMerchantId(), "1.0", "JSON", postDataHex, checkValue);

        JSONObject json = JSON.parseObject(resp);
        if (json == null || !"SUCCESS".equals(json.getString("Status"))) {
            return false;
        }
        String resultHex = json.getString("Result");
        if (resultHex == null || resultHex.isEmpty()) {
            return false;
        }
        String decrypted = EzPayEncryptUtil.decrypt(resultHex, config.getHashKey(), config.getHashIV());
        boolean exist = decrypted != null && decrypted.contains("IsExist=Y");
        // decrypted 形如 CellphoneBarcode=%2FABC1234&IsExist=Y
        log.info("[EzPay] checkBarCode decrypted={}, isExist={}", decrypted, exist);
        return exist;
    }

    /**
     * 手机条码验证（详细版，调试用）：返回完整请求/回应/解密/真实 IsExist，供 Swagger 排查。
     * 与 {@link #checkBarCode} 不同：本方法返回 ezPay 真实 IsExist（Y/N），不做任何兜底。
     */
    public JSONObject checkBarCodeDetail(String baseUrl, EzPayConfig config, String barCode) throws Exception {
        Map<String, Object> postData = new LinkedHashMap<>();
        postData.put("TimeStamp", String.valueOf(System.currentTimeMillis() / 1000L));
        postData.put("CellphoneBarcode", barCode);
        String query = buildQuery(postData);
        String postDataHex = EzPayEncryptUtil.encrypt(query, config.getHashKey(), config.getHashIV());
        String checkValue = EzPayEncryptUtil.genCheckValue(postDataHex, config.getHashKey(), config.getHashIV());
        log.info("[EzPay] checkBarCodeDetail 全量参数 >>> MerchantID={}, Version=1.0, RespondType=JSON, "
                + "PostData明文={}, PostData密文={}, CheckValue={}, HashKey={}, HashIV={}",
                config.getMerchantId(), postData, postDataHex, checkValue, config.getHashKey(), config.getHashIV());
        String resp = postFormWithCheck(baseUrl + URL_CHECK_BARCODE, config.getMerchantId(), "1.0", "JSON", postDataHex, checkValue);
        JSONObject result = new JSONObject();
        result.put("barCode", barCode);
        result.put("merchantId", config.getMerchantId());
        result.put("url", baseUrl + URL_CHECK_BARCODE);
        result.put("rawResponse", resp);
        JSONObject json = JSON.parseObject(resp);
        String status = json == null ? "" : json.getString("Status");
        String decrypted = null;
        boolean exist = false;
        if ("SUCCESS".equals(status)) {
            String resultHex = json.getString("Result");
            if (resultHex != null && !resultHex.isEmpty()) {
                decrypted = EzPayEncryptUtil.decrypt(resultHex, config.getHashKey(), config.getHashIV());
                exist = decrypted != null && decrypted.contains("IsExist=Y");
            }
        }
        result.put("status", status);
        result.put("decrypted", decrypted);
        result.put("isExist", exist);
        log.info("[EzPay] checkBarCodeDetail <<< decrypted={}, isExist={}", decrypted, exist);
        return result;
    }

    /**
     * 底层调用：补充 TimeStamp → http_build_query → PostData_ 加密 → Form Post → JSON。
     *
     * <p>作废 / 折让 / 触发开立等接口可直接复用本方法，调用方自行设置 RespondType / Version / 业务字段。
     *
     * @param fullUrl 完整请求地址（baseUrl + 接口路径常量）
     * @param config  商店配置
     * @param postData PostData_ 内含字段（不含 TimeStamp，本方法自动补；其余公共字段由调用方放进去）
     * @return ezPay 回应 JSON
     */
    public JSONObject doPost(String fullUrl, EzPayConfig config, Map<String, Object> postData) throws Exception {
        postData.put("TimeStamp", String.valueOf(System.currentTimeMillis() / 1000L));
        log.info("[EzPay] doPost 明文 >>> url={}, 明文参数={}", fullUrl, postData);
        String query = buildQuery(postData);
        String postDataEncrypted = EzPayEncryptUtil.encrypt(query, config.getHashKey(), config.getHashIV());
        String resp = postForm(fullUrl, config.getMerchantId(), postDataEncrypted);
        return JSON.parseObject(resp);
    }

    /**
     * 模拟 PHP http_build_query：key、value 均做 URL 编码，用 & 连接。
     * null 值跳过（ezPay 字段缺省时传空字符串而非 null）。
     */
    private static String buildQuery(Map<String, Object> params) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> e : params.entrySet()) {
            if (e.getValue() == null) {
                continue;
            }
            if (!sb.isEmpty()) {
                sb.append('&');
            }
            sb.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8))
                    .append('=')
                    .append(URLEncoder.encode(String.valueOf(e.getValue()), StandardCharsets.UTF_8));
        }
        return sb.toString();
    }

    /**
     * 以标准 Form Post 发送 MerchantID_ + PostData_ 两个参数。
     */
    private static String postForm(String url, String merchantId, String postData) throws Exception {
        List<NameValuePair> params = new ArrayList<>();
        params.add(new BasicNameValuePair("MerchantID_", merchantId));
        params.add(new BasicNameValuePair("PostData_", postData));
        HttpPost post = new HttpPost(url);
        post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
        log.info("[EzPay] postForm >>> url={}, body={}", url, EntityUtils.toString(post.getEntity(), StandardCharsets.UTF_8));
        try (CloseableHttpClient client = HttpClients.createDefault();
             CloseableHttpResponse res = client.execute(post)) {
            String resp = EntityUtils.toString(res.getEntity(), StandardCharsets.UTF_8);
            log.info("[EzPay] postForm <<< resp={}", resp);
            return resp;
        }
    }

    /**
     * 以标准 Form Post 发送 MerchantID_ + PostData_ + CheckValue 三个参数。
     *
     * <p>仅 BDV 验证类接口（checkBarCode / checkLoveCode）请求需要带 CheckValue。
     */
    private static String postFormWithCheck(String url, String merchantId, String version, String respondType, String postData, String checkValue) throws Exception {
        List<NameValuePair> params = new ArrayList<>();
        params.add(new BasicNameValuePair("MerchantID_", merchantId));
        params.add(new BasicNameValuePair("Version", version));
        params.add(new BasicNameValuePair("RespondType", respondType));
        params.add(new BasicNameValuePair("PostData_", postData));
        params.add(new BasicNameValuePair("CheckValue", checkValue));
        HttpPost post = new HttpPost(url);
        post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
        log.info("[EzPay] postFormWithCheck >>> url={}, body={}", url, EntityUtils.toString(post.getEntity(), StandardCharsets.UTF_8));
        try (CloseableHttpClient client = HttpClients.createDefault();
             CloseableHttpResponse res = client.execute(post)) {
            String resp = EntityUtils.toString(res.getEntity(), StandardCharsets.UTF_8);
            log.info("[EzPay] postFormWithCheck <<< resp={}", resp);
            return resp;
        }
    }
}
