package cn.quant.baa.pay.acquirer.weixin;

import cn.quant.baa.pay.acquirer.AcquirerProperties;
import cn.quant.baa.pay.acquirer.ChannelResponse;
import cn.quant.baa.pay.acquirer.MerchantAcquirer;
import cn.quant.baa.pay.config.DictionaryViewer;
import cn.quant.baa.pay.jpa.entity.TransactionSummaryEntity;
import cn.quant.baa.pay.model.dto.DictionaryItemDTO;
import cn.quant.baa.pay.model.web.*;
import cn.quant.spring.NullException;
import cn.quant.spring.ProfileException;
import cn.quant.spring.util.StringUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

import static cn.quant.baa.pay.Constant.DICT_TYPE_WXP_CALLBACK_URL;
import static cn.quant.baa.pay.acquirer.AcquirerConstant.*;

/**
 * Created by Administrator on 2021/8/31 0031.
 */
public class WeiXinMerchantAcquirer extends MerchantAcquirer {

    private static final Logger logger = LoggerFactory.getLogger(WeiXinMerchantAcquirer.class);

    private WebClient webClient;

    /**
     * 微信签名格式定义
     */
    private static final String TOKEN_PATTERN = "mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"";

    /**
     * 微信签名前缀
     */
    private static final String SCHEMA = "WECHATPAY2-SHA256-RSA2048 ";

    /**
     * 商户私钥
     */
    private PrivateKey privateKey;

    /**
     * 平台证书
     */
    private X509Certificate payCerKey;

    private static final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 支付初始化
     * 1. 初始化私钥
     * 2. 初始化平台证书
     * 3. 初始化webClient
     *
     * @param properties
     * @return
     * @throws Exception
     */
    @Override
    public MerchantAcquirer init(AcquirerProperties properties) throws Exception {
        super.init(properties);

        privateKey = getPrivateKey(properties.getPrivateKey());

        payCerKey = loadCertificate(properties.getPayCertKey());

        this.webClient = WebClient.builder().baseUrl(properties.getDomain())
                .defaultHeader("Accept", "application/json")
                .defaultHeader("User-Agent", "QuantGroup PayCenter Java Api")
                .build();
        return this;
    }

    @Override
    public Object code() {
        return this.properties.getMchChanId().toString();
    }

    @Override
    public ChannelResponse pay(PayRequestData request, TransactionSummaryEntity transaction) throws Exception {
        ObjectNode bodyNode = objectMapper.createObjectNode();
        // 转换金额为分

        DictionaryItemDTO callback = DictionaryViewer.get(DICT_TYPE_WXP_CALLBACK_URL, properties.getAccessCode().name());
        if (callback == null) {
            throw new ProfileException("Configuration not found in dictionary : {}; {}; {};", DICT_TYPE_WXP_CALLBACK_URL, properties.getAccessCode().name(), request);
        }

        BigInteger amount = new BigDecimal(request.getAmount()).multiply(new BigDecimal(100)).toBigInteger();
        bodyNode.put("mchid", properties.getPayAcctId())
                .put("appid", properties.getPayAppId())
                .put("attach", request.getAttach())
                .put("description", request.getSubject())
                .put("notify_url", String.join("/", callback.getValue(), Long.toHexString(Long.valueOf(request.getChanId()))
                        , Long.toHexString(transaction.getAccountId()), transaction.getExternalOrderNo()))
                .put("out_trade_no", transaction.getTransactionId().toString());
        bodyNode.putObject("amount")
                .put("total", amount);
        ObjectNode sceneInfo = objectMapper.createObjectNode()
                .put("payer_client_ip", request.getRequestIp());
        switch (properties.getAccessCode()) {
            case H5:
                sceneInfo.putObject("h5_info").put("type", "Wap");
                break;
            case JS:
                bodyNode.putObject("payer")
                        .put("openid", transaction.getAccountNo());
                break;
            case APP:
            default:
        }
        bodyNode.set("scene_info", sceneInfo);
        String payAccess = properties.getPayAccess();
        ChannelResponse response = doExecute(EXECUTE_PAY, payAccess, request, bodyNode, transaction);
        JsonNode node = response.getNode();
        if (node != null) {
            node = response.getNode().get("h5_url");
            if (node != null) {
                response.setNotification(node.asText());
            }
            node = response.getNode().get("prepay_id");
            if (node != null) {
                response.setNotification(node.asText());
            }
            //TODO:he APP、小程序、H5、JS不同的方式设置对应的返回参数
        }
        return response;
    }

    @Override
    public ChannelResponse checkPay(CheckPayRequestData checkPayRequestData) throws Exception {
        ObjectNode bodyNode = objectMapper.createObjectNode();
        bodyNode.put("mchid", properties.getPayAcctId());
        String payAccess = properties.getPayAccess().replace("{out_trade_no}", checkPayRequestData.getOutTradeNo());
        return doExecute(EXECUTE_CHECK_PAY, payAccess, null, bodyNode, null);
    }

    @Override
    public ChannelResponse checkRefund(CheckRefundRequestData checkRefundRequestData) throws Exception {
        String payAccess = properties.getPayAccess().replace("{out_refund_no}", checkRefundRequestData.getOutRefundNo());
        return doExecute(EXECUTE_CHECK_REFUND, payAccess, null, null, null);
    }

    @Override
    public ChannelResponse close(CloseRequestData closeRequestData) throws Exception {
        ObjectNode bodyNode = objectMapper.createObjectNode();
        bodyNode.put("mchid", properties.getPayAcctId());
        String payAccess = properties.getPayAccess().replace("{out_trade_no}", closeRequestData.getOutTradeNo());
        return doExecute(EXECUTE_CLOSE, payAccess, null, bodyNode, null);
    }

    @Override
    public ChannelResponse refund(RefundRequestData refundRequestData) throws Exception {
        // 转换金额为分
        BigInteger amount = new BigDecimal(refundRequestData.getRefundAmount()).multiply(new BigDecimal(100)).toBigInteger();
        ObjectNode bodyNode = objectMapper.createObjectNode();
        bodyNode.put("out_trade_no", refundRequestData.getOutTradeNo());
        bodyNode.put("out_refund_no", refundRequestData.getOutRefundNo());
        bodyNode.put("reason", refundRequestData.getRefundReason());
//        bodyNode.put("notify_url", "");
        bodyNode.putObject("amount")
                .put("refund", amount)
                .put("total", amount)
                .put("currency", "CNY");
        String payAccess = properties.getPayAccess();
        return doExecute(EXECUTE_REFUND, payAccess, null, bodyNode, null);
    }

    /**
     * @param access
     * @param bodyNode
     * @return
     */
    private ChannelResponse doExecute(String execute, String access, Object request, ObjectNode bodyNode, TransactionSummaryEntity history) throws Exception {

        ChannelResponse response = null;

        String method = properties.getAccessMethod();
        UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(access);
        String requestBody = "";
        // 处理GET请求
        if (HttpMethod.GET.name().equals(method)) {
            if (null != bodyNode) {
                bodyNode.fields().forEachRemaining((entry) -> {
                    uriComponentsBuilder.queryParam(entry.getKey(), entry.getValue().asText());
                });
            }
        } else {
            // 非GET请求的时候处理requestBody

            if (null != bodyNode) {
                requestBody = objectMapper.writeValueAsString(bodyNode);
            }

        }
        String uri = uriComponentsBuilder.build().toString();
        long timestamp = System.currentTimeMillis() / 1000;
        String nonceStr = history.getCheckCode();
        // 代签名字符串
        String signText = joining("\n", method, uri, String.valueOf(timestamp), nonceStr, requestBody);
        String sign = sign(signText);
        String token = String.format(TOKEN_PATTERN, properties.getPayAcctId(), nonceStr, timestamp, properties.getPayCertNo(), sign);
        Mono<ClientResponse> mono = webClient.method(HttpMethod.resolve(method))
                .uri(uri)
                .header("Authorization", SCHEMA.concat(token))
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(requestBody)
                .exchange();
        ClientResponse clientResponse = mono.block();


//            if (null == body) {
//                throw new NullException("http response body is null.");
//            }

//            if (response.statusCode().value() < 200 || response.statusCode().value() >= 300) {
//                throw new HttpServerErrorException(response.statusCode());
//            } else {
//
//            }

//            if (null == body) {
//                throw new NullException("http response body is null.");
//            } else {
        response = response(execute, access, request, clientResponse);

        //签名信息
//                    String wsSignText = joining("\n", wxTimestamp, nonce, body);
//                String wsSignText = String.join("\n", wxTimestamp, nonce, body);
//                if (verify(wsSignText, signature)) {
////                    return bodyJsonNode;
//                }
        // TODO: 签名校验失败，做异常处理
//                throw new RuntimeException("签名校验失败");
//                } else {
////                return bodyJsonNode;
//                    // TODO: 返回异常，做异常处理
////                throw new RuntimeException("返回异常，做异常处理");
//                }
//        }
//        return bodyJsonNode;
        return response;
    }

    private ChannelResponse response(ChannelResponse response) {
        if (response.getSuccess()) {

        } else {
            DictionaryItemDTO itemDTO = DictionaryViewer.get("WXP_HTTP_STAT", response.getCode());
            if (itemDTO == null) {
                response.setCode(COMMON_ERROR_CODE);
                response.setMessage(COMMON_ERROR_CODE);
            } else {
                response.setCode(itemDTO.getValue());
                response.setMessage(itemDTO.getText());
            }
        }
        return response;
    }

    private ChannelResponse response(String execute, String access, Object request, ClientResponse response) {

        if (response == null) {
            throw new NullException("Client response is null : {}; {}; {};", execute, access, request);
        }

        ChannelResponse responseData = new ChannelResponse();

        HttpHeaders httpHeaders = response.headers().asHttpHeaders();

        //Request-ID
        responseData.setRequestId(httpHeaders.getFirst("Request-ID"));
        //当前使用的微信平台证书序列号
        responseData.setSerial(httpHeaders.getFirst("Wechatpay-Serial"));
        //微信服务器的时间戳
        responseData.setTimestamp(httpHeaders.getFirst("Wechatpay-Timestamp"));
        //微信服务器提供的随机串
        responseData.setNonce(httpHeaders.getFirst("Wechatpay-Nonce"));
        //微信平台签名
        responseData.setSignature(httpHeaders.getFirst("Wechatpay-Signature"));

        responseData.setSuccess(false);

        Mono<String> resultMono = response.bodyToMono(String.class);
        String body = resultMono.blockOptional().orElse("");
        JsonNode jsonNode = null;
        try {
            jsonNode = objectMapper.readTree(body);
            JsonNode codeNode = jsonNode.get("code");
            if (codeNode == null) {
                HttpStatus httpStatus = response.statusCode();
                if ((EXECUTE_CLOSE.equals(execute) && httpStatus.value() == 204)) {
                    responseData.setSuccess(true);
                    responseData.setNode(jsonNode);
                } else if (httpStatus.value() == 200) {
                    responseData.setSuccess(true);
                    responseData.setNode(jsonNode);
                } else {
                    responseData.setSuccess(false);
                    responseData.setCode(codeNode.asText(COMMON_ERROR_CODE));
                    responseData.setMessage(StringUtils.format("Client response http status : {}; {}; {}; {};", execute, access, request, httpStatus));
                    logger.warn(responseData.getMessage());
                }
            } else {
                responseData.setSuccess(false);
                responseData.setCode(codeNode.asText(COMMON_ERROR_CODE));
                responseData.setMessage(jsonNode.get("message").asText(""));
                logger.warn("Client response body code : {}; {}; {}; {};", execute, access, request, responseData);
            }

        } catch (Exception e) {
            logger.warn(StringUtils.format("Object mapper read response : {}; {}; {};", execute, access, request), e);
            responseData.setSuccess(false);
            responseData.setCode(COMMON_ERROR_CODE);
            responseData.setMessage(e.getMessage());
            return responseData;
        }

        return response(responseData);
    }

    /**
     * 签名
     *
     * @param signText 需要签名的字符串
     * @return
     */
    public String sign(String signText) {
        try {
            Signature sign = Signature.getInstance(properties.getSignType());
            sign.initSign(this.privateKey);
            sign.update(signText.getBytes());
            return Base64.getEncoder().encodeToString(sign.sign());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
        } catch (SignatureException e) {
            throw new RuntimeException("签名计算失败", e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException("无效的私钥", e);
        }
    }

    /**
     * 获取证书。
     *
     * @param certString 证书内容
     * @return X509证书
     */
    public X509Certificate loadCertificate(String certString) {
        try {
            ByteArrayInputStream fis = new ByteArrayInputStream(certString.getBytes());
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(fis);
            cert.checkValidity();
            return cert;
        } catch (CertificateExpiredException var3) {
            throw new RuntimeException("证书已过期", var3);
        } catch (CertificateNotYetValidException var4) {
            throw new RuntimeException("证书尚未生效", var4);
        } catch (CertificateException var5) {
            throw new RuntimeException("无效的证书", var5);
        }
    }

    /**
     * RSA验签名检查
     *
     * @param content   待签名数据
     * @param signature 签名值
     * @return 布尔值
     */
    public boolean verify(String content, String signature) {
        try {
            Signature sign = Signature.getInstance(properties.getSignType());
            sign.initVerify(payCerKey);
            sign.update(content.getBytes());
            return sign.verify(Base64.getDecoder().decode(signature));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
        } catch (SignatureException e) {
            throw new RuntimeException("签名验证过程发生了错误", e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException("无效的证书", e);
        }
    }

    /**
     * 获取私钥。
     *
     * @param certString 私钥文件内容(required)
     * @return 私钥对象
     */
    public PrivateKey getPrivateKey(String certString) {
        try {
            String privateKey = certString
                    .replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(
                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    /**
     * 获取公钥。
     *
     * @param certString 公钥文件内容(required)
     * @return 私钥对象
     */
    public static PublicKey getPublicKey(String certString) throws IOException {
        try {
            String publicKey = certString
                    .replace("-----BEGIN PUBLIC KEY-----", "")
                    .replace("-----END PUBLIC KEY-----", "")
                    .replaceAll("\\s+", "");

            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePublic(
                    new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }

    /**
     * 字符串数组拼接为字符串
     *
     * @param separator 分隔符
     * @param str       字符数组
     * @return 字符串
     */
    public String joining(String separator, String... str) {
        StringBuilder builder = new StringBuilder();
        for (String s : str) {
            builder.append(s).append(separator);
        }
        return builder.toString();
    }

}