package com.xunwang.payment.service.impl;

import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.xunwang.payment.config.WxPayConfig;
import com.xunwang.payment.entity.OrderInfo;
import com.xunwang.payment.entity.Refund;
import com.xunwang.payment.enums.OrderStatus;
import com.xunwang.payment.enums.wxpay.WxApiType;
import com.xunwang.payment.enums.wxpay.WxNotifyType;
import com.xunwang.payment.enums.wxpay.WxRefundStatus;
import com.xunwang.payment.enums.wxpay.WxTradeState;
import com.xunwang.payment.service.OrderInfoService;
import com.xunwang.payment.service.PaymentInfoService;
import com.xunwang.payment.service.RefundService;
import com.xunwang.payment.service.WxPayService;
import com.xunwang.servicebase.exception.XunwangException;
import com.xunwang.servicebase.utils.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
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.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Signature;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private PaymentInfoService paymentInfoService;

    @Resource
    private RefundService refundService;

    @Resource
    private WxPayConfig wxPayConfig;

    @Resource
    private CloseableHttpClient wxPayClient;

    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public Map<String, Object> jsapiPay(String inviteId) throws IOException {
        log.info("生成订单");

        // 生成订单
        OrderInfo orderInfo = orderInfoService.createOrderByInviteId(inviteId);
        String prepayId = orderInfo.getPrepayId();
        if (!StringUtils.isEmpty(prepayId)) {
            log.info("预支付交易会话id已存在");
            // 返回预支付交易id
            Map<String, Object> map = null;
            try {
                map = jsapiMap(prepayId, orderInfo.getOrderId());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return map;
        }

        log.info("调用下单API");
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.JSAPI_PAY.getType()));

        // 请求body参数
        Gson gson = new Gson();
        HashMap<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("appid", wxPayConfig.getAppid());
        paramsMap.put("mchid", wxPayConfig.getMchId());
        paramsMap.put("description", orderInfo.getRequireDetail());
        paramsMap.put("out_trade_no", orderInfo.getOrderId());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.JSAPI_NOTIFY.getType()));

        Map<String, Object> amountMap = new HashMap<>();
        amountMap.put("total", orderInfo.getRequireSalary().intValue());
        amountMap.put("currency", "CNY");

        paramsMap.put("amount", amountMap);

        Map<String, Object> payerMap = new HashMap<>();
        payerMap.put("openid", orderInfo.getOpenId());

        paramsMap.put("payer", payerMap);

        // 将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数:" + jsonParams);

        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());   //响应体
            int statusCode = response.getStatusLine().getStatusCode();  ;//响应状态码
            if (statusCode == 200) { //处理成功
                log.info("成功，返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("小程序下单失败，响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }

            // 响应结果
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            // 预支付交易会话标识
            prepayId = resultMap.get("prepay_id");

            // 保存预支付交易会话标识
            orderInfoService.savePerpayId(orderInfo.getOrderId(), prepayId);

            Map<String, Object> map = jsapiMap(prepayId, orderInfo.getOrderId());

            return map;

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            response.close();
        }
    }

    /**
     * 封装返回jsapi的map
     * @param prepayId
     * @param orderId
     * @return
     * @throws Exception
     */
    private Map<String, Object> jsapiMap(String prepayId, String orderId) throws Exception {
        // 时间戳
        String timeStamp = String.valueOf(System.currentTimeMillis());
        // 随机字符串
        String nonceStr = generate_code(32);
        // 签名
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(wxPayConfig.getAppid()).append("\n");
        stringBuffer.append(timeStamp).append("\n");
        stringBuffer.append(nonceStr).append("\n");
        stringBuffer.append("prepay_id=").append(prepayId).append("\n");

        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(wxPayConfig.getPrivateKey(wxPayConfig.getPrivateKeyPath()));
        signature.update(stringBuffer.toString().getBytes("UTF-8"));
        byte[] signBytes = signature.sign();
        String paySign = Base64.getEncoder().encodeToString(signBytes);

        // 返回预支付交易会话标识
        Map<String, Object> map = new HashMap<>();
        map.put("appId", wxPayConfig.getAppid());
        map.put("timeStamp", timeStamp);
        map.put("nonceStr", nonceStr);
        map.put("prepay_id", prepayId);
        map.put("signType", "RSA");
        map.put("paySign", paySign);
        map.put("orderNo", orderId);

        return map;
    }

    /**
     * 生成num位的随机数
     * @param num
     * @return
     */
    private String generate_code(int num) {
        String example = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        char[] exampleArr = example.toCharArray();
        String random_code = "";
        for (int i = 0; i < num; i++) {

            int index = (int)Math.floor(Math.random() * exampleArr.length);
            random_code += exampleArr[index];
        }
        return random_code;
    }

    /**
     * 处理订单
     * @param bodyMap
     * @throws GeneralSecurityException
     */
    @Override
    public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {

        log.info("处理订单");

        String plainText = decryptFromResource(bodyMap);

        // 转换明文
        Gson gson = new Gson();
        Map<String, Object> plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String)plainTextMap.get("out_trade_no");

        /*在对业务数据进行状态检查和处理之前， 要采用数据锁进行并发控制， 以避免函数重入造成的数据混乱*/
        //尝试获取锁：
        // 成功获取则立即返回true，获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()) {
            try {
                // 处理重复通知
                // 保证接口调用的幂等性：无论接口被调用多少次，产生的结果是一致的
                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
                    return;
                }

                // 更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

                // 记录支付日志
                paymentInfoService.createPaymentInfo(plainText);
            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }
    }

    /**
     * 用户取消订单
     * @param orderNo
     * @throws IOException
     */
    @Override
    public void cancelOrder(String orderNo) throws IOException {

        // 调用微信支付的关单接口
        this.closeOrder(orderNo);

        // 更新商户端的订单状态
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
    }

    @Override
    public String queryOrder(String orderNo) throws IOException {
        log.info("查单接口调用 ===> {}", orderNo);

        String url = String.format(WxApiType.QUERY_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());

        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
            int statusCode = response.getStatusLine().getStatusCode();//响应状态码
            if (statusCode == 200) { //处理成功
                log.info("成功, 返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("查单接口调用失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }

            return bodyAsString;

        } finally {
            response.close();
        }
    }

    @Override
    public void checkOrderStatus(String orderNo) throws IOException {
        log.warn("根据订单号核实订单状态 ===> {}", orderNo);

        // 调用微信支付查单接口
        String result = this.queryOrder(orderNo);

        Gson gson = new Gson();
        Map resultMap = gson.fromJson(result, HashMap.class);

        // 获取微信支付端的订单状态
        Object tradeState = resultMap.get("trade_state");

        // 判断订单状态
        if (WxTradeState.SUCCESS.getType().equals(tradeState)) {

            log.warn("核实订单已支付 ===> {}", orderNo);

            // 如果确实订单已支付则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
            // 记录支付日志
            paymentInfoService.createPaymentInfo(result);
        }

        if (WxTradeState.NOTPAY.getType().equals(tradeState)) {
            log.warn("核实订单未支付 ===> {}", orderNo);

            // 如果订单未支付，则调用关单接口
            this.closeOrder(orderNo);

            // 更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
        }
    }

    /**
     * 退款
     * @param orderNo
     * @param reason
     * @throws IOException
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void refund(String orderNo, String reason) throws IOException {
        log.info("创建退款单记录");
        // 根据订单编号创建退款单
        Refund refundInfo = refundService.createRefundByOrderNo(orderNo, reason);

        log.info("调用退款API");

        // 调用统一退单API
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);

        // 请求body参数
        Gson gson = new Gson();
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("out_trade_no", orderNo); // 订单编号
        paramsMap.put("out_refund_no", refundInfo.getRefundNo());   // 退款单编号
        paramsMap.put("reason", reason);    // 退款原因
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));

        Map<String, Object> amountMap = new HashMap<>();
        amountMap.put("refund", refundInfo.getRefund().intValue());    // 退款金额
        amountMap.put("total", refundInfo.getTotalFee().intValue());   // 原订单金额
        amountMap.put("currency", "CNY");   // 退款币种
        paramsMap.put("amount", amountMap);

        // 将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数 ===> {}" + jsonParams);

        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");  // 设置请求报文格式
        httpPost.setEntity(entity); // 将请求报文放入请求对象中
        httpPost.setHeader("Accept", "application/json");   // 设置响应报文格式

        //完成签名并执行请求，并进行验签
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            // 解析响应结果
            String bodyAsString = EntityUtils.toString(response.getEntity());   //响应体
            int statusCode = response.getStatusLine().getStatusCode();
            ;//响应状态码
            if (statusCode == 200) { //处理成功
                log.info("成功，返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("退单接口调用失败，响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
              //  throw new IOException("request failed");
                throw new XunwangException(ResultCode.ERROR, "退单接口调用失败，响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
            }

            // 更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);

            // 更新退款单
            refundService.updateRefund(bodyAsString);

        } finally {
            response.close();
        }
    }

    @Override
    public String queryRefund(String refundNo) throws IOException {
        log.info("查询退款接口调用 ===> {}",refundNo);

        String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
        url = wxPayConfig.getDomain().concat(url);

        // 创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        // 完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 查询退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
            }

            return bodyAsString;
        } finally {
            response.close();
        }
    }

    /**
     * 根据退款单号核实退款单状态
     * @param refundNo
     * @throws IOException
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void checkRefundStatus(String refundNo) throws IOException {

        log.warn("根据退款单号核实退款单状态 ===> {}", refundNo);

        // 调用查询退款单接口
        String result = this.queryRefund(refundNo);

        // 组装json请求体字符串
        Gson gson = new Gson();
        Map<String, String> resultMap = gson.fromJson(result, HashMap.class);

        // 获取微信支付端退款状态
        String status = resultMap.get("status");
        String orderNo = resultMap.get("out_trade_no");

        if (WxRefundStatus.SUCCESS.getType().equals(status)) {

            log.warn("核实订单已退款成功 ===> {}", refundNo);

            // 如果确定退款成功，则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);

            // 更新退款单
            refundService.updateRefund(result);
        }

        if (WxRefundStatus.ABNORMAL.getType().equals(status)) {

            log.warn("核实订单退款异常 ===> {}", refundNo);

            // 如果确定退款异常，则更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);

            // 更新退款单
            refundService.updateRefund(result);
        }
    }

    /**
     * 处理退款单
     * @param bodyMap
     * @throws GeneralSecurityException
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processRefund(Map<String, Object> bodyMap) throws GeneralSecurityException {

        log.info("退款单");

        // 解密报文
        String plainText = decryptFromResource(bodyMap);

        // 将明文转换成map
        Gson gson = new Gson();
        HashMap plainTextMap = gson.fromJson(plainText, HashMap.class);
        String orderNo = (String)plainTextMap.get("out_trade_no");

        if (lock.tryLock()) {
            try {

                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
                    return;
                }

                // 更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);

                // 更新退款单
                refundService.updateRefund(plainText);
            } finally {
                // 主要释放锁
                lock.unlock();
            }
        }
    }


    /**
     * 关单接口的调用
     * @param orderNo
     */
    private void closeOrder(String orderNo) throws IOException {

        log.info("关单接口的调用，订单号 ===> {}", orderNo);

        // 创建远程请求对象
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url);
        HttpPost httpPost = new HttpPost(url);

        // 封装json请求体
        Gson gson = new Gson();
        Map<String, String> paramsMap = new HashMap<>();
        paramsMap.put("mchid", wxPayConfig.getMchId());
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数 ===> {}", jsonParams);

        // 将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(jsonParams,"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            int statusCode = response.getStatusLine().getStatusCode();//响应状态码
            if (statusCode == 200) { //处理成功
                log.info("成功200");
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功204");
            } else {
                log.info("关闭失败,响应码 = " + statusCode);
                throw new IOException("request failed");
            }
        } finally {
            response.close();
        }
    }

    /**
     * 对称解密
     * @param bodyMap
     * @return
     * @throws GeneralSecurityException
     */
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {

        log.info("密文解密");

        // 通知数据
        Map<String, String> resourceMap = (Map) bodyMap.get("resource");
        // 数据密文
        String ciphertext = resourceMap.get("ciphertext");
        // 随机串
        String nonce = resourceMap.get("nonce");
        // 附加数据
        String associatedData = resourceMap.get("associated_data");

        log.info("密文 ===> {}", ciphertext);

        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext);

        log.info("明文 ===> {}", plainText);

        return plainText;
    }
}
