package com.ruoyi.web.modules.pay.support;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.web.modules.pay.config.WeChatMiniAppProperties;
import com.ruoyi.web.modules.pay.config.WeChatPayProperties;
import com.ruoyi.web.modules.pay.constant.PayConstants;

/**
 * 微信支付 V3 签名与 HTTP 调用
 */
@Component
public class WeChatPaySupport
{
    private static final String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";

    private static final String REFUND_URL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";

    private static final String CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";

    private final WeChatPayProperties payProperties;

    private final WeChatMiniAppProperties miniAppProperties;

    private final RestTemplate restTemplate = new RestTemplate();

    public WeChatPaySupport(WeChatPayProperties payProperties, WeChatMiniAppProperties miniAppProperties)
    {
        this.payProperties = payProperties;
        this.miniAppProperties = miniAppProperties;
    }

    public String resolveOpenidByCode(String code)
    {
        if (StringUtils.isEmpty(code))
        {
            throw new ServiceException("微信授权码无效");
        }
        if (StringUtils.isEmpty(miniAppProperties.getAppId()) || StringUtils.isEmpty(miniAppProperties.getAppSecret()))
        {
            throw new ServiceException("微信小程序未配置");
        }
        String url = CODE2SESSION_URL + "?appid=" + miniAppProperties.getAppId()
                + "&secret=" + miniAppProperties.getAppSecret()
                + "&js_code=" + code
                + "&grant_type=authorization_code";
        String body = restTemplate.getForObject(url, String.class);
        JSONObject json = JSON.parseObject(body);
        if (json == null || StringUtils.isEmpty(json.getString("openid")))
        {
            throw new ServiceException("微信登录失败");
        }
        return json.getString("openid");
    }

    public String createJsapiPrepayId(String outTradeNo, String description, BigDecimal payAmount, String openid,
            String mchId, String notifyUrl)
    {
        assertPayConfigReady();
        int totalFen = payAmount.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).intValueExact();
        JSONObject amount = new JSONObject();
        amount.put("total", totalFen);
        amount.put("currency", "CNY");
        JSONObject payer = new JSONObject();
        payer.put("openid", openid);
        JSONObject payload = new JSONObject();
        payload.put("appid", payProperties.getAppId());
        payload.put("mchid", mchId);
        payload.put("description", description);
        payload.put("out_trade_no", outTradeNo);
        payload.put("notify_url", notifyUrl);
        payload.put("amount", amount);
        payload.put("payer", payer);
        String body = payload.toJSONString();
        String response = postSigned(JSAPI_URL, body, mchId);
        JSONObject result = JSON.parseObject(response);
        if (result == null || StringUtils.isEmpty(result.getString("prepay_id")))
        {
            throw new ServiceException(PayConstants.MSG_PAY_CREATE_FAILED);
        }
        return result.getString("prepay_id");
    }

    /**
     * 微信 V3 境内退款（同步返回 SUCCESS / PROCESSING 视为受理成功）
     *
     * @return 微信 refund_id
     */
    public String createDomesticRefund(String transactionId, String outRefundNo, BigDecimal totalAmount,
            BigDecimal refundAmount, String reason, String mchId)
    {
        assertPayConfigReady();
        if (StringUtils.isEmpty(transactionId) || StringUtils.isEmpty(outRefundNo))
        {
            throw new ServiceException(PayConstants.MSG_REFUND_FAILED);
        }
        int totalFen = totalAmount.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).intValueExact();
        int refundFen = refundAmount.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).intValueExact();
        if (refundFen <= 0 || refundFen > totalFen)
        {
            throw new ServiceException(PayConstants.MSG_REFUND_AMOUNT_INVALID);
        }
        JSONObject amount = new JSONObject();
        amount.put("refund", refundFen);
        amount.put("total", totalFen);
        amount.put("currency", "CNY");
        JSONObject payload = new JSONObject();
        payload.put("transaction_id", transactionId);
        payload.put("out_refund_no", outRefundNo);
        payload.put("reason", StringUtils.isEmpty(reason) ? "售后退款" : reason);
        payload.put("amount", amount);
        String body = payload.toJSONString();
        String response = postSigned(REFUND_URL, body, mchId);
        JSONObject result = JSON.parseObject(response);
        if (result == null || StringUtils.isEmpty(result.getString("refund_id")))
        {
            throw new ServiceException(PayConstants.MSG_REFUND_FAILED);
        }
        String status = result.getString("status");
        if (!"SUCCESS".equals(status) && !"PROCESSING".equals(status))
        {
            throw new ServiceException(PayConstants.MSG_REFUND_FAILED);
        }
        return result.getString("refund_id");
    }

    public Map<String, String> buildJsapiPayParams(String prepayId)
    {
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        String packageVal = "prepay_id=" + prepayId;
        String message = payProperties.getAppId() + "\n" + timeStamp + "\n" + nonceStr + "\n" + packageVal + "\n";
        Map<String, String> params = new HashMap<>();
        params.put("appId", payProperties.getAppId());
        params.put("timeStamp", timeStamp);
        params.put("nonceStr", nonceStr);
        params.put("package", packageVal);
        params.put("signType", "RSA");
        params.put("paySign", sign(message));
        return params;
    }

    /**
     * 校验微信支付回调签名（Wechatpay-Signature）
     */
    public void verifyNotifySignature(String timestamp, String nonce, String rawBody, String signature)
    {
        if (!payProperties.isRealPayEnabled() || !payProperties.isNotifyVerifyEnabled())
        {
            return;
        }
        if (StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(nonce)
                || StringUtils.isEmpty(rawBody) || StringUtils.isEmpty(signature))
        {
            throw new ServiceException(PayConstants.MSG_PAY_NOTIFY_INVALID);
        }
        long ts;
        try
        {
            ts = Long.parseLong(timestamp);
        }
        catch (NumberFormatException ex)
        {
            throw new ServiceException(PayConstants.MSG_PAY_NOTIFY_INVALID);
        }
        long now = System.currentTimeMillis() / 1000;
        if (Math.abs(now - ts) > payProperties.getNotifyTimestampToleranceSeconds())
        {
            throw new ServiceException(PayConstants.MSG_PAY_NOTIFY_INVALID);
        }
        String message = timestamp + "\n" + nonce + "\n" + rawBody + "\n";
        try
        {
            Signature verifier = Signature.getInstance("SHA256withRSA");
            verifier.initVerify(loadPublicKey());
            verifier.update(message.getBytes(StandardCharsets.UTF_8));
            if (!verifier.verify(Base64.getDecoder().decode(signature)))
            {
                throw new ServiceException(PayConstants.MSG_PAY_NOTIFY_INVALID);
            }
        }
        catch (ServiceException ex)
        {
            throw ex;
        }
        catch (Exception ex)
        {
            throw new ServiceException(PayConstants.MSG_PAY_NOTIFY_INVALID);
        }
    }

    public JSONObject decryptNotifyResource(String associatedData, String nonce, String ciphertext)
    {
        try
        {
            byte[] key = payProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8);
            byte[] nonceBytes = nonce.getBytes(StandardCharsets.UTF_8);
            byte[] cipherBytes = Base64.getDecoder().decode(ciphertext);
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(128, nonceBytes);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
            if (StringUtils.isNotEmpty(associatedData))
            {
                cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));
            }
            String plain = new String(cipher.doFinal(cipherBytes), StandardCharsets.UTF_8);
            return JSON.parseObject(plain);
        }
        catch (Exception ex)
        {
            throw new ServiceException(PayConstants.MSG_PAY_NOTIFY_INVALID);
        }
    }

    public void assertPayConfigReady()
    {
        if (StringUtils.isEmpty(payProperties.getAppId())
                || StringUtils.isEmpty(payProperties.getMchId())
                || StringUtils.isEmpty(payProperties.getApiV3Key())
                || StringUtils.isEmpty(payProperties.getMerchantSerialNo())
                || StringUtils.isEmpty(payProperties.getPrivateKeyPath())
                || StringUtils.isEmpty(payProperties.getNotifyUrl()))
        {
            throw new ServiceException(PayConstants.MSG_PAY_CONFIG_MISSING);
        }
    }

    private String postSigned(String url, String body, String mchId)
    {
        String authorization = buildAuthorization("POST", url.replace("https://api.mch.weixin.qq.com", ""), body, mchId);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("Authorization", authorization);
        headers.set("Accept", "application/json");
        HttpEntity<String> entity = new HttpEntity<>(body, headers);
        ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
        return response.getBody();
    }

    private String buildAuthorization(String method, String canonicalUrl, String body, String mchId)
    {
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonceStr = UUID.randomUUID().toString().replace("-", "");
        String message = method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n";
        String signature = sign(message);
        return "WECHATPAY2-SHA256-RSA2048 mchid=\"" + mchId + "\",nonce_str=\"" + nonceStr
                + "\",timestamp=\"" + timestamp + "\",serial_no=\"" + payProperties.getMerchantSerialNo()
                + "\",signature=\"" + signature + "\"";
    }

    private String sign(String message)
    {
        try
        {
            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign(loadPrivateKey());
            signature.update(message.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(signature.sign());
        }
        catch (Exception ex)
        {
            throw new ServiceException(PayConstants.MSG_PAY_CONFIG_MISSING);
        }
    }

    private PrivateKey loadPrivateKey() throws Exception
    {
        String pem = readPem(payProperties.getPrivateKeyPath());
        pem = pem.replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s", "");
        byte[] decoded = Base64.getDecoder().decode(pem);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
        return KeyFactory.getInstance("RSA").generatePrivate(spec);
    }

    private PublicKey loadPublicKey() throws Exception
    {
        String path = payProperties.getPlatformPublicKeyPath();
        if (StringUtils.isEmpty(path))
        {
            throw new ServiceException(PayConstants.MSG_PAY_NOTIFY_VERIFY_KEY_MISSING);
        }
        String pem = readPem(path);
        pem = pem.replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replace("-----BEGIN CERTIFICATE-----", "")
                .replace("-----END CERTIFICATE-----", "")
                .replaceAll("\\s", "");
        byte[] decoded = Base64.getDecoder().decode(pem);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
        return KeyFactory.getInstance("RSA").generatePublic(spec);
    }

    private String readPem(String path) throws IOException
    {
        if (path.startsWith("classpath:"))
        {
            try (InputStream in = new ClassPathResource(path.substring("classpath:".length())).getInputStream())
            {
                return readStream(in);
            }
        }
        return new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8);
    }

    private String readStream(InputStream in) throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        byte[] data = new byte[4096];
        int n;
        while ((n = in.read(data)) != -1)
        {
            buffer.write(data, 0, n);
        }
        return buffer.toString(StandardCharsets.UTF_8.name());
    }
}
