package com.yami.shop.wx.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.TypeReference;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import com.yami.shop.bean.bo.PayInfoBo;
import com.yami.shop.wx.config.CombinePayUrlEnum;
import com.yami.shop.wx.config.WechatPayServiceConfig;
import com.yami.shop.wx.config.WxConstants;
import com.yami.shop.wx.po.JsapiPayInfoPo;
import com.yami.shop.wx.po.RefundInfoPo;
import com.yami.shop.wx.po.TraceWaybill;
import com.yami.shop.wx.service.WxProviderService;
import com.yami.shop.wx.utils.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.Signature;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
@Service
public class WxProviderServiceImpl implements WxProviderService {

    @Autowired
    private WechatPayServiceConfig wechatPayServiceConfig;

    private static final String WX_SUCCESS_XML = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";

    private static final String REFUND_URL = "https://shop.api.zswlgz.com/notice/pay/order/refundNotify";
    // 测试环境
//    private static final String REFUND_URL = "https://63332824.r36.cpolar.top/notice/pay/order/refundNotify";

    @Override
    public Map<String, Object> subJsapi(JsapiPayInfoPo po) {
        System.out.println("微信支付传入参数===========" + po);
        Map<String, Object> params = new HashMap<>(8);
        params.put("sp_appid", wechatPayServiceConfig.getSpAppId());
        params.put("sp_mchid", wechatPayServiceConfig.getSpMchId());
        params.put("sub_appid", wechatPayServiceConfig.getSubAppId());
        params.put("sub_mchid", wechatPayServiceConfig.getSubMchId());
        Map<String, Object> payerMap = new HashMap<>(4);
        payerMap.put("sub_openid", po.getOpenId());
        params.put("payer", payerMap);
        params.put("description", po.getDescription());
        params.put("out_trade_no", po.getOutTradeNo());
        params.put("notify_url", po.getNotifyUrl());
        JSONObject attachJson = new JSONObject();
        attachJson.put("orderNo", po.getOutTradeNo());
        params.put("attach", JSONObject.toJSONString(attachJson));
        Map<String, Object> amountMap = new HashMap<>(4);
        amountMap.put("total", po.getTotal());
        amountMap.put("currency", "CNY");
        params.put("amount", amountMap);
        Map<String, Object> sceneInfoMap = new HashMap<>(4);
        sceneInfoMap.put("payer_client_ip", "127.0.0.1");
        sceneInfoMap.put("device_id", "127.0.0.1");
        params.put("scene_info", sceneInfoMap);
        String paramsStr = JSON.toJSONString(params);
        log.info("请求参数 ===> {}", paramsStr);
        String url = wechatPayServiceConfig.getBaseUrl().concat(CombinePayUrlEnum.PAY_TRANSACTIONS.getType().concat("jsapi"));
        log.info("请求地址 ===> {}", url);
        String resStr = wechatHttpPost(url, paramsStr);
        Map<String, Object> resMap = JSONObject.parseObject(resStr, new TypeReference<Map<String, Object>>() {
        });
        return paySignMsgApplet(resMap);
    }

    /**
     * 构建签名
     *
     * @param map 参数
     * @return map
     */
    private Map<String, Object> paySignMsgApplet(Map<String, Object> map) {
        long timeMillis = System.currentTimeMillis();
        String timeStamp = timeMillis / 1000 + "";
        String nonceStr = String.valueOf(timeMillis);
        String prepayId = map.get("prepay_id").toString();
        String packageStr = "prepay_id=" + prepayId;
        Map<String, Object> resMap = new HashMap<>();
        resMap.put("nonceStr", nonceStr);
        resMap.put("timeStamp", timeStamp);
        resMap.put("appId", wechatPayServiceConfig.getSpAppId());
        resMap.put("package", packageStr);
        resMap.put("paySign", getWxPayResultMap(prepayId, timeStamp, nonceStr));
        resMap.put("signType", "RSA");
        return resMap;
    }

    @SneakyThrows
    public String getWxPayResultMap(String prepayId, String timestamp, String nonceStr) {
        Signature sign = Signature.getInstance("SHA256withRSA");
        PrivateKey privateKey = getPrivateKey(wechatPayServiceConfig.getKeyPemPath());
        sign.initSign(privateKey);
        String sb = wechatPayServiceConfig.getSubAppId() + "\n" +
                timestamp + "\n" +
                nonceStr + "\n" +
                "prepay_id=" + prepayId + "\n";
        sign.update(sb.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(sign.sign());
    }

    /**
     * 订单号查询订单
     *
     * @param orderNo orderNo
     * @return Map
     */
    @Override
    public Map<String, Object> getPayResultByOrderNo(String orderNo) {
        log.info("根据订单号查询订单，订单号： {}", orderNo);
        String url = wechatPayServiceConfig.getBaseUrl().concat("/pay/partner/transactions/out-trade-no/".concat(orderNo))
                .concat("?sp_mchid=").concat(wechatPayServiceConfig.getSpMchId())
                .concat("&sub_mchid=").concat(wechatPayServiceConfig.getSubMchId());
        String res = wechatHttpGet(url);
        log.info("查询订单结果：{}", res);
        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>() {
        });
        String outTradeNo = resMap.get("out_trade_no").toString();
        String appId = resMap.get("sp_appid").toString();
        String mchId = resMap.get("sp_mchid").toString();
        String subMchId = resMap.get("sub_mchid").toString();
        // 支付后才返回参数
        Object payer = resMap.get("payer");
        Object attach = resMap.get("attach");
        /*
                  交易状态，枚举值：
                  SUCCESS：支付成功
                  REFUND：转入退款
                  NOTPAY：未支付
                  CLOSED：已关闭
                  REVOKED：已撤销（仅付款码支付会返回）
                  USERPAYING：用户支付中（仅付款码支付会返回）
                  PAYERROR：支付失败（仅付款码支付会返回）
         */
        String tradeState = resMap.get("trade_state").toString();
        log.info("outTradeNo：" + outTradeNo);
        log.info("appId：" + appId);
        log.info("mchId：" + mchId);
        log.info("tradeState：" + tradeState);
        log.info("payer：" + payer);
        log.info("payer：" + attach);
        log.info("subMchId：" + subMchId);
        return resMap;
    }

    /**
     * 申请退款
     *
     * @param po 请求参数
     * @return 退款结果
     */
    @Override
    public String refundOrder(RefundInfoPo po) {
        log.info("根据订单号申请退款，订单号： {}", po.getOutTradeNo());
        String url = wechatPayServiceConfig.getBaseUrl().concat(CombinePayUrlEnum.DOMESTIC_REFUNDS.getType());
        Map<String, Object> params = new HashMap<>(2);
        params.put("out_trade_no", po.getOutTradeNo());
        params.put("sub_mchid", wechatPayServiceConfig.getSubMchId());
        String outRefundNo = OrderUtils.getOrderNo("TK");
        log.info("退款申请号：{}", outRefundNo);
        params.put("out_refund_no", outRefundNo);
        params.put("reason", "申请退款");
        params.put("notify_url", REFUND_URL);
        Map<String, Object> amountMap = new HashMap<>();
        //退款金额，单位：分
        amountMap.put("refund", po.getRefundMoney());
        //原订单金额，单位：分
        amountMap.put("total", po.getTotal());
        amountMap.put("currency", "CNY");
        params.put("amount", amountMap);
        String paramsStr = JSON.toJSONString(params);
        log.info("请求参数 ===> {}", paramsStr);
        String res;
        try {
            res = wechatHttpPost(url, paramsStr);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
        log.info("退款结果：{}", res);
        return res;
    }

    /**
     * 查询单笔退款信息
     *
     * @param refundNo refundNo
     * @return return
     */
    @Override
    public Map<String, Object> queryRefundOrder(String refundNo) {
        log.info("根据订单号查询退款订单，订单号： {}", refundNo);
        String url = wechatPayServiceConfig.getBaseUrl()
                .concat(CombinePayUrlEnum.DOMESTIC_REFUNDS_QUERY.getType().concat(refundNo))
                .concat("?sub_mchid=").concat(wechatPayServiceConfig.getSubMchId());
        String res = wechatHttpGet(url);
        log.info("查询退款订单结果：{}", res);
        Map<String, Object> resMap = JSONObject.parseObject(res, new TypeReference<Map<String, Object>>() {
        });
        String successTime = resMap.get("success_time").toString();
        String refundId = resMap.get("refund_id").toString();
        /*
          款到银行发现用户的卡作废或者冻结了，导致原路退款银行卡失败，可前往商户平台-交易中心，手动处理此笔退款。
          枚举值：
          SUCCESS：退款成功
          CLOSED：退款关闭
          PROCESSING：退款处理中
          ABNORMAL：退款异常
         */
        String status = resMap.get("status").toString();
        /*
          枚举值：
          ORIGINAL：原路退款
          BALANCE：退回到余额
          OTHER_BALANCE：原账户异常退到其他余额账户
          OTHER_BANKCARD：原银行卡异常退到其他银行卡
         */
        String channel = resMap.get("channel").toString();
        String userReceivedAccount = resMap.get("user_received_account").toString();
        log.info("successTime：" + successTime);
        log.info("channel：" + channel);
        log.info("refundId：" + refundId);
        log.info("status：" + status);
        log.info("userReceivedAccount：" + userReceivedAccount);
        // TODO 在查询单笔退款信息时，可以再去查询一次订单的状态，保证该订单已经退款完毕了
        return resMap;
    }

    /**
     * 获取商户的私钥文件
     *
     * @param keyPemPath keyPemPath
     * @return return
     */
    public PrivateKey getPrivateKey(String keyPemPath) {
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(keyPemPath);
        if (inputStream == null) {
            throw new RuntimeException("私钥文件不存在");
        }
        return PemUtil.loadPrivateKey(inputStream);
    }

    /**
     * 获取证书管理器实例
     *
     * @return return
     */
    @SneakyThrows
    public Verifier getVerifier() {
        log.info("获取证书管理器实例");
        //获取商户私钥
        PrivateKey privateKey = getPrivateKey(wechatPayServiceConfig.getKeyPemPath());
        //私钥签名对象
        PrivateKeySigner privateKeySigner = new PrivateKeySigner(wechatPayServiceConfig.getSerialNo(), privateKey);
        //身份认证对象
        WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(wechatPayServiceConfig.getSpMchId(), privateKeySigner);
        // 使用定时更新的签名验证器，不需要传入证书
        CertificatesManager certificatesManager = CertificatesManager.getInstance();
        certificatesManager.putMerchant(wechatPayServiceConfig.getSpMchId(),
                wechatPay2Credentials, wechatPayServiceConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        return certificatesManager.getVerifier(wechatPayServiceConfig.getSpMchId());
    }

    /**
     * 获取支付http请求对象
     *
     * @return return
     */
    public CloseableHttpClient httpClient() {
        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
                .withMerchant(wechatPayServiceConfig.getSpMchId(),
                        wechatPayServiceConfig.getSerialNo(), getPrivateKey(wechatPayServiceConfig.getKeyPemPath()))
                .withValidator(new WechatPay2Validator(getVerifier()));
        // 通过WechatPayHttpClientBuilder构造的HttpClient，会自动的处理签名和验签，并进行证书自动更新
        return builder.build();
    }

    /**
     * 发送get请求
     *
     * @param url 请求地址
     * @return 结果
     */
    public String wechatHttpGet(String url) {
        CloseableHttpClient wxPayClient = httpClient();
        try {
            HttpGet httpGet = new HttpGet(url);
            httpGet.setHeader("Accept", "application/json");
            CloseableHttpResponse response = wxPayClient.execute(httpGet);
            return getResponseBody(response);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 发送post请求
     *
     * @param url       请求地址
     * @param paramsStr 参数
     * @return 结果
     */
    public String wechatHttpPost(String url, String paramsStr) {
        CloseableHttpClient wxPayClient = httpClient();
        try {
            HttpPost httpPost = new HttpPost(url);
            StringEntity entity = new StringEntity(paramsStr, "utf-8");
            entity.setContentType("application/json");
            httpPost.setEntity(entity);
            httpPost.setHeader("Accept", "application/json");
            CloseableHttpResponse response = wxPayClient.execute(httpPost);
            return getResponseBody(response);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 读取响应的请求体
     *
     * @param response 响应
     * @return 响应
     * @throws IOException IOException
     */
    private String getResponseBody(CloseableHttpResponse response) throws IOException {
        HttpEntity entity = response.getEntity();
        String body = entity == null ? "" : EntityUtils.toString(entity);
        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) {
            log.info("成功, 返回结果 = " + body);
        } else {
            String msg = "微信支付请求失败,响应码 = " + statusCode + ",返回结果 = " + body;
            log.error(msg);
            throw new RuntimeException(msg);
        }
        return body;
    }

    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public PayInfoBo notifyParse(HttpServletRequest request, HttpServletResponse response) {
        JSONObject bodyJson = getNotifyBodyJson(request);
        PayInfoBo bo = new PayInfoBo();
        if (lock.tryLock()) {
            try {
                // 解密resource中的通知数据
                String resource = bodyJson.getString("resource");
                JSONObject resourceJson = WechatPayValidator.decryptFromResource(resource, wechatPayServiceConfig.getApiV3Key());
                System.out.println("=================== 服务商小程序支付回调解密resource中的通知数据 ===================\n" + resource);
                if (resourceJson.getString("trade_state").equals("SUCCESS")) {
                    bo.setPayNo(resourceJson.getString("out_trade_no"));
                    bo.setBizPayNo(resourceJson.getString("transaction_id"));
                    bo.setIsPaySuccess(true);
                    bo.setSuccessString(WX_SUCCESS_XML);
                    return bo;
                } else {
                    System.err.println("支付失败...");
                }
            } finally {
                lock.unlock();
            }
        }
        return null;
    }

    private final ReentrantLock refundLock = new ReentrantLock();

    @Override
    public void refundNotifyParse(HttpServletRequest request, HttpServletResponse response) {
        JSONObject bodyJson = getNotifyBodyJson(request);
        if (refundLock.tryLock()) {
            try {
                // 解密resource中的通知数据
                String resource = bodyJson.getString("resource");
                JSONObject resourceJson = WechatPayValidator.decryptFromResource(resource, wechatPayServiceConfig.getApiV3Key());
                System.out.println("=================== 服务商小程序退款回调解密resource中的通知数据 ===================\n" + resource);
                if (resourceJson.getString("refund_status").equals("SUCCESS")) {
                    //TODO 业务处理
                    resourceJson.get("out_trade_no");
                    resourceJson.get("transaction_id");
                    resourceJson.get("out_refund_no");
                    resourceJson.get("refund_id");
                }
            } finally {
                refundLock.unlock();
            }
        }
    }

    private JSONObject getNotifyBodyJson(HttpServletRequest request) {
        String body = WechatPayValidator.readData(request);
        log.info("===========微信回调参数===========\n" + body);
        log.info("微信回调参数：{}", body);
        JSONObject jsonObject = JSONObject.parseObject(body);
        WechatPayValidator wechatPayValidator = new WechatPayValidator(getVerifier(), jsonObject.getString("id"), body);
        CullenUtils.validateDataThrowException(!wechatPayValidator.validate(request),"回调通知验签失败...");
        log.info("通知验签成功");
        return jsonObject;
    }

    public WechatPayServiceConfig getWxConfig() {
        return wechatPayServiceConfig;
    }

    /**
     * 小程序发货信息录入接口
     *
     * @param trackingNo     物流单号
     * @param transactionId  原支付交易对应的微信订单号
     * @param expressCompany 物流公司编码 例如：SF
     * @param itemDesc       例如：微信红包抱枕*1个，限120个字以内
     * @param phone          当发货的物流公司为顺丰时，联系方式为必填，收件人或寄件人联系方式二选一
     * @param openid         微信open_id-小程序
     * @param type           物流模式，发货方式枚举值：1、实体物流配送采用快递公司进行实体物流配送形式
     *                       2、同城配送 3、虚拟商品，虚拟商品，例如话费充值，点卡等，无实体配送形式 4、用户自提
     */
    public static void uploadShippingInfo(String trackingNo, String transactionId, String itemDesc,
                                          String phone, String openid, String expressCompany, Integer type) {
        AccessToken accessToken = AccessTokenApi.getAccessToken(WxConstants.APPLET_APPID, WxConstants.APPLET_SECRET);
        String url = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=" + accessToken.getToken();
        // 定义日期时间格式化对象
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
        // 格式化日期时间
        String formattedDateTime = formatter.format(DateUtils.getNowDate());
        // 构建要上传的物流信息
        JSONObject requestDataJson = new JSONObject();
        JSONObject orderKeyJson = new JSONObject();
        orderKeyJson.put("order_number_type", 2);
        orderKeyJson.put("transaction_id", transactionId);
        requestDataJson.put("order_key", orderKeyJson);

        requestDataJson.put("logistics_type", type);//物流或者自提
        requestDataJson.put("upload_time", formattedDateTime);
        requestDataJson.put("delivery_mode", 1);

        ArrayList<JSONObject> shippingList = new ArrayList<>();
        if (trackingNo == null) {
            JSONObject shippingJson = new JSONObject();
            shippingJson.put("item_desc", itemDesc);
            shippingList.add(shippingJson);
        } else {
            String[] strArray = trackingNo.replaceAll("\\s+", "").split(",");
            if (strArray.length > 1) {
                requestDataJson.put("delivery_mode", 2);
                requestDataJson.put("is_all_delivered", true);
            }
            for (String tracking : strArray) {
                JSONObject shippingJson = new JSONObject();
                shippingJson.put("tracking_no", tracking);
                shippingJson.put("express_company", expressCompany);
                shippingJson.put("item_desc", itemDesc);
                JSONObject consignorJson = new JSONObject();
                consignorJson.put("consignor_contact", phone);
                shippingJson.put("contact", consignorJson);
                shippingList.add(shippingJson);
            }
        }
        requestDataJson.put("shipping_list", shippingList);
        JSONObject phoneJson = new JSONObject();
        phoneJson.put("openid", openid);
        requestDataJson.put("payer", phoneJson);

        System.err.println(" ==========请求参数========= " + requestDataJson.toJSONString());
        try {
            CloseableHttpClient httpClient = HttpClients.createDefault();
            HttpPost httpPost = new HttpPost(url);
            httpPost.addHeader("Content-Type", "application/json");
            StringEntity requestBody = new StringEntity(requestDataJson.toJSONString(), ContentType.APPLICATION_JSON);
            httpPost.setEntity(requestBody);
            CloseableHttpResponse response = httpClient.execute(httpPost);
            HttpEntity responseEntity = response.getEntity();
            String responseString = EntityUtils.toString(responseEntity);
            System.out.println(" ==========返回结果========= " + responseString);
            log.info(responseString);
            response.close();
            httpClient.close();
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }


    @SneakyThrows
    public static String traceWaybill(TraceWaybill traceWaybill) {
        AccessToken accessToken = AccessTokenApi.getAccessToken(WxConstants.APPLET_APPID, WxConstants.APPLET_SECRET);
        String url = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/trace_waybill?access_token=" + accessToken.getToken();
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(url);
        httpPost.addHeader("Content-Type", "application/json");
        StringEntity requestBody = new StringEntity(new Gson().toJson(traceWaybill), ContentType.APPLICATION_JSON.withCharset("UTF-8"));
        httpPost.setEntity(requestBody);
        CloseableHttpResponse response = httpClient.execute(httpPost);
        HttpEntity responseEntity = response.getEntity();
        String responseString = EntityUtils.toString(responseEntity, "UTF-8");
        log.info(responseString);
        response.close();
        httpClient.close();
        return responseString;
    }

    @SneakyThrows
    public static String queryTrace(String waybillToken) {
        JSONObject json = new JSONObject();
        json.put("waybill_token",waybillToken);
        AccessToken accessToken = AccessTokenApi.getAccessToken(WxConstants.APPLET_APPID, WxConstants.APPLET_SECRET);
        String url = "https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/query_trace?access_token=" + accessToken.getToken();
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(url);
        httpPost.addHeader("Content-Type", "application/json");
        StringEntity requestBody = new StringEntity(json.toString(), ContentType.APPLICATION_JSON.withCharset("UTF-8"));
        httpPost.setEntity(requestBody);
        CloseableHttpResponse response = httpClient.execute(httpPost);
        HttpEntity responseEntity = response.getEntity();
        String responseString = EntityUtils.toString(responseEntity, "UTF-8");
        log.info(responseString);
        response.close();
        httpClient.close();
        return responseString;
    }


    public static void main(String[] args) {

    }
}
