package com.ruoyi.wx.web.controller;


import com.github.binarywang.wxpay.bean.notify.OriginNotifyResponse;
import com.github.binarywang.wxpay.bean.notify.SignatureHeader;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
import com.github.binarywang.wxpay.bean.request.WxPayOrderCloseRequest;
import com.github.binarywang.wxpay.bean.request.WxPayOrderQueryRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayOrderCloseResult;
import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.google.gson.GsonBuilder;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.WxUser;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.recovery.domain.WxRecharge;
import com.ruoyi.framework.recovery.service.impl.WxRechargeServiceImpl;
import com.ruoyi.framework.recovery.service.impl.WxUserServiceImpl;
import com.ruoyi.wx.web.domain.dto.WxPayDto;
import com.ruoyi.wx.web.unit.WeixinNotifyUtils;
import io.swagger.annotations.ApiOperation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * 支付接口
 */
@Slf4j
@RestController
@RequestMapping("/wx/pay")
public class WxPayController {
    @Autowired
    private WxPayService wxPayService;

    @Autowired
    private WxRechargeServiceImpl wxRechargeService;

    @Autowired
    private WxUserServiceImpl wxUserService;

    private final ReentrantLock orderLock = new ReentrantLock();
    /**
     * <pre>
     * 查询订单(详见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2)
     * 该接口提供所有微信支付订单的查询，商户可以通过查询订单接口主动查询订单状态，完成下一步的业务逻辑。
     * 需要调用查询接口的情况：
     * ◆ 当商户后台、网络、服务器等出现异常，商户系统最终未接收到支付通知；
     * ◆ 调用支付接口后，返回系统错误或未知交易状态情况；
     * ◆ 调用被扫支付API，返回USERPAYING的状态；
     * ◆ 调用关单或撤销接口API之前，需确认支付状态；
     * 接口地址：https://api.mch.weixin.qq.com/pay/orderquery
     * </pre>
     *
     * @param transactionId 微信订单号
     * @param outTradeNo    商户系统内部的订单号，当没提供transactionId时需要传这个。
     */
    @ApiOperation(value = "查询订单")
    @GetMapping("/query/order")
    public WxPayOrderQueryResult queryOrder(@RequestParam(required = false) String transactionId,
                                            @RequestParam(required = false) String outTradeNo)
            throws WxPayException {
        return this.wxPayService.queryOrder(transactionId, outTradeNo);
    }


    @ApiOperation(value = "查询订单")
    @PostMapping("/query/order")
    public WxPayOrderQueryResult queryOrder(@RequestBody WxPayOrderQueryRequest wxPayOrderQueryRequest) throws WxPayException {
        return this.wxPayService.queryOrder(wxPayOrderQueryRequest);
    }


    /**
     * <pre>
     * 关闭订单
     * 应用场景
     * 以下情况需要调用关单接口：
     * 1. 商户订单支付失败需要生成新单号重新发起支付，要对原订单号调用关单，避免重复支付；
     * 2. 系统下单后，用户支付超时，系统退出不再受理，避免用户继续，请调用关单接口。
     * 注意：订单生成后不能马上调用关单接口，最短调用时间间隔为5分钟。
     * 接口地址：https://api.mch.weixin.qq.com/pay/closeorder
     * 是否需要证书：   不需要。
     * </pre>
     *
     * @param outTradeNo 商户系统内部的订单号
     */
    @ApiOperation(value = "关闭订单")
    @GetMapping("/closeOrder/{outTradeNo}")
    public WxPayOrderCloseResult closeOrder(@PathVariable String outTradeNo) throws WxPayException {
        return this.wxPayService.closeOrder(outTradeNo);
    }

    @ApiOperation(value = "关闭订单")
    @PostMapping("/closeOrder")
    public WxPayOrderCloseResult closeOrder(@RequestBody WxPayOrderCloseRequest wxPayOrderCloseRequest) throws WxPayException {
        return this.wxPayService.closeOrder(wxPayOrderCloseRequest);
    }

    /**
     * 调用统一下单接口，并组装生成支付所需参数对象.
     *
     * @param dto 统一下单请求参数
     * @return 返回 {@link com.github.binarywang.wxpay.bean.order}包下的类对象
     */
    @ApiOperation(value = "统一下单，并组装所需支付参数")
    @PostMapping("/createOrder")
    public AjaxResult createOrder(@RequestBody WxPayDto dto) throws WxPayException {
        log.info("开始下单：{}", dto);
        WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
        request.setDescription("购买会员");
        // 商品订单号
        String orderNo = "ORDER_" + (System.currentTimeMillis() + 1);
        request.setOutTradeNo(orderNo);
        // 订单金额
        WxPayUnifiedOrderV3Request.Amount amount = new WxPayUnifiedOrderV3Request.Amount();
        amount.setCurrency("CNY");
        BigDecimal total = new BigDecimal(dto.gettBal());
        if (StringUtils.isNotEmpty(dto.getMoney())) {
            total = total.subtract(new BigDecimal(dto.getMoney()));
        } else if (StringUtils.isNotEmpty(dto.getDiscount())) {
            total = total.multiply(new BigDecimal(dto.getDiscount()).divide(new BigDecimal(10)));
        }
        amount.setTotal(total.intValue() * 100);
        request.setAmount(amount);
        log.info("用户：{}，支付总金额为:{}, 订单Id为:{}", dto.getOpenId(), amount, orderNo);
        WxPayUnifiedOrderV3Request.Payer payer = new WxPayUnifiedOrderV3Request.Payer();
        payer.setOpenid(dto.getOpenId());
        request.setPayer(payer);
        WxPayUnifiedOrderV3Result.JsapiResult orderV3 = this.wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request);
        log.info("支付内容:{}", orderV3);
        WxRecharge wxRecharge = new WxRecharge();
        wxRecharge.setPoints(dto.getPoints());
        wxRecharge.setPrice(total);
        wxRecharge.setPayTime(new Date());
        WxUser wxUser = wxUserService.selectWxUSerByOpenid(dto.getOpenId());
        wxRecharge.setUserId(wxUser.getId());
        wxRecharge.setUserName(wxUser.getUsername());
        wxRecharge.setPayStat("1");
        wxRecharge.setOrderNo(orderNo);
        wxRecharge.setVipLv(dto.getVipLv());
        wxRechargeService.insertWxRecharge(wxRecharge);
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("appId", orderV3.getAppId());
        resultMap.put("timeStamp", orderV3.getTimeStamp());
        resultMap.put("nonceStr", orderV3.getNonceStr());
        resultMap.put("packageValue", orderV3.getPackageValue());
        resultMap.put("signType", orderV3.getSignType());
        resultMap.put("paySign", orderV3.getPaySign());
        resultMap.put("outTradeNo", orderNo);
        return AjaxResult.success(resultMap);
    }

    /**
     * 统一下单(详见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1)
     * 在发起微信支付前，需要调用统一下单接口，获取"预支付交易会话标识"
     * 接口地址：https://api.mch.weixin.qq.com/pay/unifiedorder
     *
     * @param request 请求对象，注意一些参数如appid、mchid等不用设置，方法内会自动从配置对象中获取到（前提是对应配置中已经设置）
     */
    @ApiOperation(value = "原生的统一下单接口")
    @PostMapping("/unifiedOrder")
    public WxPayUnifiedOrderResult unifiedOrder(@RequestBody WxPayUnifiedOrderRequest request) throws WxPayException {
        return this.wxPayService.unifiedOrder(request);
    }

    /**
     * 回调接口
     */
    @ApiOperation(value = "回调接口")
    @PostMapping("/notify")
    public AjaxResult notify(HttpServletRequest request, HttpServletResponse response, @RequestBody String notifyData) throws WxPayException {
        log.debug("======= 接收到通知 =========");
        // 获取请求头信息
        SignatureHeader signatureHeader = WeixinNotifyUtils.getSignatureHeader(request);
        // 将请求体json字符串转换为实体
        OriginNotifyResponse notifyResponse = new GsonBuilder().create().fromJson(notifyData, OriginNotifyResponse.class);
        // 支付成功通知
        if ("TRANSACTION.SUCCESS".equals(notifyResponse.getEventType())) {
            // 获取锁
            if (orderLock.tryLock()) {
                try {
                    // 解析支付结果通知
                    WxPayNotifyV3Result result = wxPayService.parseOrderNotifyV3Result(notifyData, signatureHeader);
                    String orderNo = result.getResult().getOutTradeNo();
                    WxRecharge wxRecharge = wxRechargeService.selectWxRechargeByOrderId(orderNo);
                    wxRecharge.setPayStat("2");
                    wxRechargeService.updateWxRecharge(wxRecharge);
                    log.info("查询支付信息：{}", wxRecharge);
                    WxUser wxUser = wxUserService.selectWxUserById(wxRecharge.getUserId());
                    wxUser.setVipLv(wxRecharge.getVipLv());
                    wxUser.setVipStartTime(null == wxUser.getVipStartTime() ? LocalDate.now(ZoneId.of("Asia/Shanghai")) : wxUser.getVipStartTime());
                    wxUser.setVipEndTime(defDate(null == wxUser.getVipEndTime() ? wxUser.getVipStartTime() : wxUser.getVipEndTime(), wxUser.getVipLv()));
                    log.info("用户信息：{}", wxUser);
                    wxUserService.updateWxUser(wxUser);
                    log.debug("支付成功 ---> orderNo：" + result.getResult().getOutTradeNo());
                    // 返回成功（无需数据，系统状态码为200即可）
                    return AjaxResult.success();
                } catch (Exception e) {
                    // 支付结果解析异常/订单处理异常
                    log.error("支付通知处理异常：", e);
                    response.setStatus(500);// 支付成功通知处理失败时需要将状态码修改为5xx/4xx，微信才会重新发送回调
                    return AjaxResult.error();
                } finally {
                    // 释放锁
                    orderLock.unlock();
                }
            } else {
                // 锁获取失败，返回异常，等待下次消息
                response.setStatus(503);// 支付成功通知处理失败时需要将状态码修改为5xx/4xx，微信才会重新发送回调
                return AjaxResult.error();
            }
        }
        return AjaxResult.success();
    }

    private static LocalDate defDate(LocalDate now, Integer offset) {
        LocalDate defDate = null;
        switch (offset) {
            case 1:
                defDate = now.plusMonths(1);
                break;
            case 2:
                defDate = now.plusMonths(3);
                break;
            case 3:
                defDate = now.plusMonths(12);
                break;
            default:
                break;
        }
        return defDate;
    }
}
