package com.hotwj.platform.pay.service.impl;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.databind.JsonNode;
import com.hotwj.platform.pay.controller.vo.PayRechargeChannelRespVO;
import com.hotwj.platform.pay.controller.vo.PayRechargeStatusRespVO;
import com.hotwj.platform.pay.controller.vo.PayRechargeUnifiedOrderReqVO;
import com.hotwj.platform.pay.controller.vo.PayRechargeUnifiedOrderRespVO;
import com.hotwj.platform.pay.controller.vo.PayWalletBalanceRespVO;
import com.hotwj.platform.pay.domain.PayApp;
import com.hotwj.platform.pay.domain.PayChannel;
import com.hotwj.platform.pay.domain.PayOrder;
import com.hotwj.platform.pay.domain.PayOrderExtension;
import com.hotwj.platform.pay.domain.PayRechargeOrder;
import com.hotwj.platform.pay.domain.PayWallet;
import com.hotwj.platform.pay.domain.PayWalletTransaction;
import com.hotwj.platform.pay.enums.PayChannelCodeEnum;
import com.hotwj.platform.pay.enums.PayOrderBizTypeEnum;
import com.hotwj.platform.pay.enums.PayOrderStatusEnum;
import com.hotwj.platform.pay.mapper.PayAppMapper;
import com.hotwj.platform.pay.mapper.PayChannelMapper;
import com.hotwj.platform.pay.mapper.PayOrderExtensionMapper;
import com.hotwj.platform.pay.mapper.PayOrderMapper;
import com.hotwj.platform.pay.mapper.PayRechargeOrderMapper;
import com.hotwj.platform.pay.mapper.PayWalletMapper;
import com.hotwj.platform.pay.mapper.PayWalletTransactionMapper;
import com.hotwj.platform.pay.service.PayRechargeOpenService;
import com.hotwj.platform.pay.utils.PayCryptoUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class PayRechargeOpenServiceImpl implements PayRechargeOpenService {

    private static final String DEFAULT_PAY_APP_KEY = "wallet-recharge";
    private static final String ALIPAY_GATEWAY = "https://openapi.alipay.com/gateway.do";
    private static final String WECHAT_GATEWAY = "https://api.mch.weixin.qq.com";
    private static final DateTimeFormatter ALIPAY_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final int ORDER_EXPIRE_MINUTES = 3;
    private static final int COMPENSATE_BATCH_SIZE = 50;
    private static final long WALLET_BIZ_TYPE_RECHARGE = 1L;
    private static final long WALLET_FREEZE_STATUS_NORMAL = 0L;
    private static final long WALLET_FREEZE_STATUS_FROZEN = 1L;

    private final PayAppMapper payAppMapper;
    private final PayChannelMapper payChannelMapper;
    private final PayOrderMapper payOrderMapper;
    private final PayOrderExtensionMapper payOrderExtensionMapper;
    private final PayRechargeOrderMapper payRechargeOrderMapper;
    private final PayWalletMapper payWalletMapper;
    private final PayWalletTransactionMapper payWalletTransactionMapper;

    @Override
    public List<PayRechargeChannelRespVO> listRechargeChannels() {
        PayApp payApp = getActivePayApp();
        List<PayChannel> channels = payChannelMapper.selectList(Wrappers.<PayChannel>lambdaQuery()
                .eq(PayChannel::getAppId, payApp.getId())
                .eq(PayChannel::getStatus, 1L)
                .orderByAsc(PayChannel::getId));
        if (channels == null || channels.isEmpty()) {
            return List.of();
        }
        List<PayRechargeChannelRespVO> result = new ArrayList<>();
        for (PayChannel channel : channels) {
            PayChannelCodeEnum codeEnum = Arrays.stream(PayChannelCodeEnum.values())
                    .filter(item -> StrUtil.equalsIgnoreCase(item.getCode(), channel.getCode()))
                    .findFirst()
                    .orElse(null);
            if (codeEnum == null) {
                continue;
            }
            PayRechargeChannelRespVO respVO = new PayRechargeChannelRespVO();
            respVO.setProvider(codeEnum.getProvider());
            respVO.setScene(codeEnum.getScene());
            respVO.setChannelCode(codeEnum.getCode());
            respVO.setPayMethod(codeEnum.getPayMethod());
            respVO.setPayScene(codeEnum.getPayScene());
            respVO.setDisplayName(buildChannelDisplayName(codeEnum));
            result.add(respVO);
        }
        return result;
    }

    @Override
    public PayWalletBalanceRespVO queryWalletBalance(Long walletType) {
        Long userId = LoginHelper.getUserId();
        Long companyId = LoginHelper.getCompanyId();
        if (userId == null || companyId == null) {
            throw new ServiceException("登录状态异常，请重新登录后查询钱包余额");
        }
        Long actualWalletType = walletType == null ? 2L : walletType;
        Long walletUserId = resolveWalletUserId(companyId, userId, actualWalletType);
        PayWallet wallet = payWalletMapper.selectOne(Wrappers.<PayWallet>lambdaQuery()
                .eq(PayWallet::getUserId, walletUserId)
                .eq(PayWallet::getWalletType, actualWalletType)
                .last("limit 1"));
        PayWalletBalanceRespVO respVO = new PayWalletBalanceRespVO();
        respVO.setWalletType(actualWalletType);
        respVO.setBalance(wallet == null || wallet.getBalance() == null ? BigDecimal.ZERO : wallet.getBalance());
        respVO.setFreezeStatus(wallet == null || wallet.getFreezeStatus() == null ? WALLET_FREEZE_STATUS_NORMAL : wallet.getFreezeStatus());
        return respVO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public PayRechargeUnifiedOrderRespVO unifiedOrder(PayRechargeUnifiedOrderReqVO reqVO, HttpServletRequest request) {
        Long userId = LoginHelper.getUserId();
        Long companyId = LoginHelper.getCompanyId();
        if (userId == null || companyId == null) {
            throw new ServiceException("登录状态异常，请重新登录后发起支付");
        }

        PayChannelCodeEnum channelCodeEnum = PayChannelCodeEnum.of(reqVO.getProvider(), reqVO.getScene());
        PayApp payApp = getActivePayApp();
        PayChannel payChannel = getActiveChannel(payApp.getId(), channelCodeEnum.getCode());
        PayWallet wallet = getOrCreateWallet(companyId, userId, reqVO.getWalletType());
        ensureWalletAvailable(wallet, "钱包已冻结，无法充值");
        checkDuplicatePendingRechargeOrder(userId, wallet.getId());

        BigDecimal amount = normalizeAmount(reqVO.getAmount());
        Date expireTime = DateUtil.offsetMinute(new Date(), ORDER_EXPIRE_MINUTES);
        String rechargeOrderNo = buildNo("RC");
        String payOrderNo = buildNo("PO");
        String extensionNo = buildNo("PX");
        String notifyUrl = buildNotifyUrl(request, reqVO.getProvider(), payChannel.getId());

        PayRechargeOrder rechargeOrder = new PayRechargeOrder();
        rechargeOrder.setCompanyId(companyId);
        rechargeOrder.setAccountId(wallet.getId());
        rechargeOrder.setUserId(userId);
        rechargeOrder.setRechargeOrderNo(rechargeOrderNo);
        rechargeOrder.setRechargeAmount(amount);
        rechargeOrder.setPayMethod(channelCodeEnum.getPayMethod());
        rechargeOrder.setPayScene(channelCodeEnum.getPayScene());
        rechargeOrder.setPayStatus(PayOrderStatusEnum.WAITING.getStatus());
        rechargeOrder.setRemark("钱包充值");
        payRechargeOrderMapper.insert(rechargeOrder);

        PayOrder payOrder = new PayOrder();
        payOrder.setAppId(payApp.getId());
        payOrder.setMerchantOrderNo(rechargeOrderNo);
        payOrder.setBizOrderType(PayOrderBizTypeEnum.FUND_RECHARGE.getCode());
        payOrder.setBizOrderId(rechargeOrder.getId());
        payOrder.setSubject("钱包充值");
        payOrder.setBody("用户/企业钱包余额充值");
        payOrder.setNotifyUrl(notifyUrl);
        payOrder.setReturnUrl(reqVO.getReturnUrl());
        payOrder.setPrice(amount);
        payOrder.setStatus(PayOrderStatusEnum.WAITING.getStatus());
        payOrder.setUserIp(ServletUtils.getClientIP(request));
        payOrder.setExpireTime(expireTime);
        payOrder.setNo(payOrderNo);
        payOrderMapper.insert(payOrder);

        PayOrderExtension extension = new PayOrderExtension();
        extension.setNo(extensionNo);
        extension.setOrderId(payOrder.getId());
        extension.setChannelId(payChannel.getId());
        extension.setChannelCode(payChannel.getCode());
        extension.setUserIp(payOrder.getUserIp());
        extension.setStatus(PayOrderStatusEnum.WAITING.getStatus());
        payOrderExtensionMapper.insert(extension);

        payOrder.setExtensionId(extension.getId());
        payOrderMapper.updateById(payOrder);

        String rawResponse;
        PayRechargeUnifiedOrderRespVO respVO = new PayRechargeUnifiedOrderRespVO();
        respVO.setRechargeOrderNo(rechargeOrderNo);
        respVO.setPayOrderNo(payOrderNo);
        respVO.setChannelCode(payChannel.getCode());
        respVO.setExpireTime(expireTime);

        if ("ALIPAY".equalsIgnoreCase(reqVO.getProvider())) {
            AlipayChannelConfig config = parseAlipayConfig(payChannel.getConfig());
            rawResponse = buildAlipayPayPayload(config, channelCodeEnum, extensionNo, amount, payOrder.getSubject(), notifyUrl, reqVO.getReturnUrl(), respVO);
        } else {
            WechatChannelConfig config = parseWechatConfig(payChannel.getConfig());
            rawResponse = buildWechatPayPayload(config, channelCodeEnum, extensionNo, amount, payOrder.getSubject(), notifyUrl, payOrder.getUserIp(), respVO);
        }

        extension.setChannelExtras(rawResponse);
        payOrderExtensionMapper.updateById(extension);
        PayRechargeOrder updateRechargeOrder = new PayRechargeOrder();
        updateRechargeOrder.setId(rechargeOrder.getId());
        updateRechargeOrder.setPayStatus(PayOrderStatusEnum.PAYING.getStatus());
        payRechargeOrderMapper.updateById(updateRechargeOrder);
        return respVO;
    }

    @Override
    public PayRechargeStatusRespVO queryRechargeStatus(String rechargeOrderNo) {
        PayRechargeOrder rechargeOrder = payRechargeOrderMapper.selectOne(Wrappers.<PayRechargeOrder>lambdaQuery()
                .eq(PayRechargeOrder::getRechargeOrderNo, rechargeOrderNo)
                .last("limit 1"));
        if (rechargeOrder == null) {
            throw new ServiceException("充值订单不存在");
        }
        PayOrder payOrder = payOrderMapper.selectOne(Wrappers.<PayOrder>lambdaQuery()
                .eq(PayOrder::getBizOrderType, PayOrderBizTypeEnum.FUND_RECHARGE.getCode())
                .eq(PayOrder::getBizOrderId, rechargeOrder.getId())
                .last("limit 1"));
        PayRechargeStatusRespVO respVO = new PayRechargeStatusRespVO();
        respVO.setRechargeOrderNo(rechargeOrder.getRechargeOrderNo());
        respVO.setPayOrderNo(payOrder == null ? null : payOrder.getNo());
        respVO.setPayStatus(rechargeOrder.getPayStatus());
        respVO.setAmount(rechargeOrder.getRechargeAmount());
        respVO.setThirdTradeNo(rechargeOrder.getThirdTradeNo());
        respVO.setPaidTime(rechargeOrder.getPaidTime());
        return respVO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String handleAlipayNotify(Long channelId, Map<String, String> params) {
        PayChannel payChannel = getChannelById(channelId);
        AlipayChannelConfig config = parseAlipayConfig(payChannel.getConfig());

        String sign = params.get("sign");
        if (StrUtil.isBlank(sign)) {
            throw new ServiceException("支付宝回调签名为空");
        }
        Map<String, String> signParams = new TreeMap<>();
        params.forEach((key, value) -> {
            if (!"sign".equals(key) && !"sign_type".equals(key) && StrUtil.isNotBlank(value)) {
                signParams.put(key, value);
            }
        });
        String content = buildPlainQuery(signParams, false);
        if (!PayCryptoUtils.rsaVerify(content, sign, config.getAlipayPublicKey())) {
            throw new ServiceException("支付宝回调验签失败");
        }

        String tradeStatus = params.get("trade_status");
        if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
            LocalDateTime paidTime = LocalDateTime.parse(params.get("gmt_payment"), ALIPAY_TIME_FORMATTER);
            markOrderPaid(params.get("out_trade_no"), params.get("trade_no"), params.get("buyer_id"),
                    JsonUtils.toJsonString(params), Date.from(paidTime.atZone(ZoneId.systemDefault()).toInstant()));
        } else if ("TRADE_CLOSED".equals(tradeStatus)) {
            markOrderClosed(params.get("out_trade_no"), params.get("trade_no"), JsonUtils.toJsonString(params));
        }
        return "success";
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String handleWechatNotify(Long channelId, String body, HttpServletRequest request) {
        try {
            PayChannel payChannel = getChannelById(channelId);
            WechatChannelConfig config = parseWechatConfig(payChannel.getConfig());
            String timestamp = ServletUtils.getHeader(request, "Wechatpay-Timestamp");
            String nonce = ServletUtils.getHeader(request, "Wechatpay-Nonce");
            String signature = ServletUtils.getHeader(request, "Wechatpay-Signature");
            String verifyMessage = timestamp + "\n" + nonce + "\n" + body + "\n";
            if (!PayCryptoUtils.rsaVerify(verifyMessage, signature, config.getPlatformPublicKey())) {
                throw new ServiceException("微信回调验签失败");
            }

            JsonNode root = JsonUtils.getObjectMapper().readTree(body);
            JsonNode resource = root.path("resource");
            String plainText = PayCryptoUtils.aesGcmDecrypt(config.getApiV3Key(),
                    resource.path("associated_data").asText(),
                    resource.path("nonce").asText(),
                    resource.path("ciphertext").asText());
            JsonNode notify = JsonUtils.getObjectMapper().readTree(plainText);
            String tradeState = notify.path("trade_state").asText();
            String outTradeNo = notify.path("out_trade_no").asText();
            if ("SUCCESS".equals(tradeState)) {
                Date paidTime = Date.from(OffsetDateTime.parse(notify.path("success_time").asText()).toInstant());
                markOrderPaid(outTradeNo, notify.path("transaction_id").asText(), notify.path("payer").path("openid").asText(""),
                        plainText, paidTime);
            } else if ("CLOSED".equals(tradeState) || "PAYERROR".equals(tradeState)) {
                markOrderClosed(outTradeNo, notify.path("transaction_id").asText(), plainText);
            }
            return "{\"code\":\"SUCCESS\",\"message\":\"成功\"}";
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new ServiceException("处理微信支付回调失败: " + e.getMessage());
        }
    }

    @Override
    public void compensatePayOrders() {
        Date now = new Date();
        List<PayOrder> recentOrders = payOrderMapper.selectList(Wrappers.<PayOrder>lambdaQuery()
                .in(PayOrder::getStatus, PayOrderStatusEnum.WAITING.getStatus(), PayOrderStatusEnum.PAYING.getStatus())
                .gt(PayOrder::getExpireTime, now)
                .ge(PayOrder::getCreateTime, DateUtil.offsetMinute(now, -ORDER_EXPIRE_MINUTES))
                .orderByAsc(PayOrder::getCreateTime)
                .last("limit " + COMPENSATE_BATCH_SIZE));
        List<PayOrder> expiredOrders = payOrderMapper.selectList(Wrappers.<PayOrder>lambdaQuery()
                .in(PayOrder::getStatus, PayOrderStatusEnum.WAITING.getStatus(), PayOrderStatusEnum.PAYING.getStatus())
                .isNotNull(PayOrder::getExpireTime)
                .le(PayOrder::getExpireTime, now)
                .orderByAsc(PayOrder::getExpireTime)
                .last("limit " + COMPENSATE_BATCH_SIZE));

        int queried = 0;
        int closed = 0;
        for (PayOrder payOrder : recentOrders) {
            if (compensateSingleOrder(payOrder, false)) {
                queried++;
            }
        }
        for (PayOrder payOrder : expiredOrders) {
            if (compensateSingleOrder(payOrder, true)) {
                closed++;
            }
        }
        if (queried > 0 || closed > 0) {
            log.info("支付补偿任务完成：主动查单 {} 笔，超时关单 {} 笔", queried, closed);
        }
    }

    private String buildAlipayPayPayload(AlipayChannelConfig config, PayChannelCodeEnum channelCodeEnum,
                                         String extensionNo, BigDecimal amount, String subject, String notifyUrl,
                                         String returnUrl, PayRechargeUnifiedOrderRespVO respVO) {
        Map<String, Object> bizContent = new LinkedHashMap<>();
        bizContent.put("out_trade_no", extensionNo);
        bizContent.put("total_amount", amount.setScale(2, RoundingMode.HALF_UP).toPlainString());
        bizContent.put("subject", subject);
        bizContent.put("timeout_express", ORDER_EXPIRE_MINUTES + "m");

        if (PayChannelCodeEnum.ALIPAY_QR == channelCodeEnum) {
            String response = invokeAlipayOpenApi(config, "alipay.trade.precreate", bizContent, notifyUrl, null);
            try {
                JsonNode root = JsonUtils.getObjectMapper().readTree(response);
                JsonNode node = root.path("alipay_trade_precreate_response");
                if (!"10000".equals(node.path("code").asText())) {
                    throw new ServiceException("支付宝下单失败: " + node.path("sub_msg").asText(node.path("msg").asText()));
                }
                respVO.setDisplayMode("QR_CODE");
                respVO.setCodeUrl(node.path("qr_code").asText());
                return response;
            } catch (Exception e) {
                throw new ServiceException("解析支付宝预下单响应失败: " + e.getMessage());
            }
        }

        if (PayChannelCodeEnum.ALIPAY_H5 == channelCodeEnum) {
            bizContent.put("product_code", "QUICK_WAP_WAY");
            String payUrl = buildAlipayRedirectUrl(config, "alipay.trade.wap.pay", bizContent, notifyUrl, returnUrl);
            respVO.setDisplayMode("REDIRECT");
            respVO.setPayUrl(payUrl);
            return payUrl;
        }

        bizContent.put("product_code", "QUICK_MSECURITY_PAY");
        String orderString = buildAlipayAppOrderString(config, "alipay.trade.app.pay", bizContent, notifyUrl);
        respVO.setDisplayMode("APP");
        respVO.setOrderString(orderString);
        return orderString;
    }

    private String buildWechatPayPayload(WechatChannelConfig config, PayChannelCodeEnum channelCodeEnum,
                                         String extensionNo, BigDecimal amount, String subject, String notifyUrl,
                                         String userIp, PayRechargeUnifiedOrderRespVO respVO) {
        try {
            Map<String, Object> bodyMap = new LinkedHashMap<>();
            bodyMap.put("appid", config.getAppId());
            bodyMap.put("mchid", config.getMerchantId());
            bodyMap.put("description", subject);
            bodyMap.put("out_trade_no", extensionNo);
            bodyMap.put("notify_url", notifyUrl);
            bodyMap.put("amount", Map.of("total", toFen(amount), "currency", "CNY"));
            bodyMap.put("time_expire", toOffsetDateTime(DateUtil.offsetMinute(new Date(), ORDER_EXPIRE_MINUTES)));

            String path;
            if (PayChannelCodeEnum.WECHAT_QR == channelCodeEnum) {
                path = "/v3/pay/transactions/native";
                bodyMap.put("scene_info", Map.of("payer_client_ip", userIp));
            } else if (PayChannelCodeEnum.WECHAT_H5 == channelCodeEnum) {
                path = "/v3/pay/transactions/h5";
                bodyMap.put("scene_info", Map.of(
                        "payer_client_ip", userIp,
                        "h5_info", Map.of("type", "Wap")
                ));
            } else {
                path = "/v3/pay/transactions/app";
            }

            String requestBody = JsonUtils.toJsonString(bodyMap);
            String response = invokeWechatApi(config, "POST", path, requestBody);
            JsonNode node = JsonUtils.getObjectMapper().readTree(response);
            if (PayChannelCodeEnum.WECHAT_QR == channelCodeEnum) {
                respVO.setDisplayMode("QR_CODE");
                respVO.setCodeUrl(node.path("code_url").asText());
                return response;
            }
            if (PayChannelCodeEnum.WECHAT_H5 == channelCodeEnum) {
                respVO.setDisplayMode("REDIRECT");
                respVO.setPayUrl(node.path("h5_url").asText());
                return response;
            }
            String prepayId = node.path("prepay_id").asText();
            String nonceStr = IdUtil.fastSimpleUUID();
            String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
            String packageValue = "Sign=WXPay";
            String message = config.getAppId() + "\n" + timestamp + "\n" + nonceStr + "\n" + prepayId + "\n";
            String sign = PayCryptoUtils.rsaSign(message, config.getPrivateKey());
            Map<String, String> appParams = new LinkedHashMap<>();
            appParams.put("appid", config.getAppId());
            appParams.put("partnerid", config.getMerchantId());
            appParams.put("prepayid", prepayId);
            appParams.put("package", packageValue);
            appParams.put("noncestr", nonceStr);
            appParams.put("timestamp", timestamp);
            appParams.put("sign", sign);
            respVO.setDisplayMode("APP");
            respVO.setAppPayParams(appParams);
            return JsonUtils.toJsonString(appParams);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new ServiceException("微信支付下单失败: " + e.getMessage());
        }
    }

    private String invokeAlipayOpenApi(AlipayChannelConfig config, String method, Map<String, Object> bizContent,
                                       String notifyUrl, String returnUrl) {
        Map<String, String> params = buildAlipayCommonParams(config, method, notifyUrl, returnUrl);
        params.put("biz_content", JsonUtils.toJsonString(bizContent));
        params.put("sign", signAlipayParams(params, config.getPrivateKey()));
        String response = HttpRequest.post(defaultIfBlank(config.getGateway(), ALIPAY_GATEWAY))
                .header(Header.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=UTF-8")
                .body(buildPlainQuery(params, true))
                .execute()
                .body();
        if (StrUtil.isBlank(response)) {
            throw new ServiceException("支付宝网关返回为空");
        }
        return response;
    }

    private String buildAlipayRedirectUrl(AlipayChannelConfig config, String method, Map<String, Object> bizContent,
                                          String notifyUrl, String returnUrl) {
        Map<String, String> params = buildAlipayCommonParams(config, method, notifyUrl, returnUrl);
        params.put("biz_content", JsonUtils.toJsonString(bizContent));
        params.put("sign", signAlipayParams(params, config.getPrivateKey()));
        return defaultIfBlank(config.getGateway(), ALIPAY_GATEWAY) + "?" + buildPlainQuery(params, true);
    }

    private String buildAlipayAppOrderString(AlipayChannelConfig config, String method, Map<String, Object> bizContent, String notifyUrl) {
        Map<String, String> params = buildAlipayCommonParams(config, method, notifyUrl, null);
        params.put("biz_content", JsonUtils.toJsonString(bizContent));
        params.put("sign", signAlipayParams(params, config.getPrivateKey()));
        return buildPlainQuery(params, true);
    }

    private Map<String, String> buildAlipayCommonParams(AlipayChannelConfig config, String method, String notifyUrl, String returnUrl) {
        Map<String, String> params = new TreeMap<>();
        params.put("app_id", config.getAppId());
        params.put("method", method);
        params.put("charset", "UTF-8");
        params.put("sign_type", "RSA2");
        params.put("timestamp", DateUtil.format(new Date(), DatePattern.NORM_DATETIME_PATTERN));
        params.put("version", "1.0");
        params.put("notify_url", notifyUrl);
        if (StrUtil.isNotBlank(returnUrl)) {
            params.put("return_url", returnUrl);
        }
        return params;
    }

    private String signAlipayParams(Map<String, String> params, String privateKey) {
        return PayCryptoUtils.rsaSign(buildPlainQuery(params, false), privateKey);
    }

    private String invokeWechatApi(WechatChannelConfig config, String method, String path, String body) {
        String nonceStr = IdUtil.fastSimpleUUID();
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String message = method + "\n" + path + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n";
        String signature = PayCryptoUtils.rsaSign(message, config.getPrivateKey());
        String authorization = String.format(
                "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%s\",serial_no=\"%s\",signature=\"%s\"",
                config.getMerchantId(), nonceStr, timestamp, config.getMerchantSerialNo(), signature
        );
        HttpResponse httpResponse = HttpRequest.of(defaultIfBlank(config.getGateway(), WECHAT_GATEWAY) + path)
                .method(cn.hutool.http.Method.valueOf(method))
                .header("Authorization", authorization)
                .header("Accept", "application/json")
                .header("Content-Type", "application/json")
                .body(body)
                .execute();
        String response = httpResponse.body();
        if (StrUtil.isBlank(response) && httpResponse.getStatus() != 204) {
            throw new ServiceException("微信网关返回为空");
        }
        return StrUtil.blankToDefault(response, "");
    }

    private boolean compensateSingleOrder(PayOrder payOrder, boolean expired) {
        try {
            PaymentOrderContext context = buildOrderContext(payOrder);
            PaymentChannelResult result = queryRemoteOrder(context);
            if (result == null) {
                return false;
            }
            if (result.isSuccess()) {
                markOrderPaid(context.extension.getNo(), result.thirdTradeNo, result.channelUserId, result.rawData, result.paidTime);
                return false;
            }
            if (result.isClosed()) {
                markOrderClosed(context.extension.getNo(), result.thirdTradeNo, result.rawData);
                return expired;
            }
            if (expired) {
                closeRemoteOrder(context);
                markOrderClosed(context.extension.getNo(), result.thirdTradeNo, result.rawData);
                return true;
            }
            return true;
        } catch (Exception e) {
            log.warn("支付补偿处理失败 payOrderNo={} expired={}", payOrder.getNo(), expired, e);
            return false;
        }
    }

    private PaymentOrderContext buildOrderContext(PayOrder payOrder) {
        PayOrderExtension extension = payOrderExtensionMapper.selectOne(Wrappers.<PayOrderExtension>lambdaQuery()
                .eq(PayOrderExtension::getOrderId, payOrder.getId())
                .orderByDesc(PayOrderExtension::getId)
                .last("limit 1"));
        if (extension == null) {
            throw new ServiceException("支付拓展单不存在: " + payOrder.getNo());
        }
        PayChannel channel = getChannelById(extension.getChannelId());
        return new PaymentOrderContext(payOrder, extension, channel);
    }

    private PaymentChannelResult queryRemoteOrder(PaymentOrderContext context) throws Exception {
        if (isAlipayChannel(context.channel.getCode())) {
            return queryAlipayOrder(context);
        }
        if (isWechatChannel(context.channel.getCode())) {
            return queryWechatOrder(context);
        }
        throw new ServiceException("暂不支持的支付渠道: " + context.channel.getCode());
    }

    private void closeRemoteOrder(PaymentOrderContext context) throws Exception {
        if (isAlipayChannel(context.channel.getCode())) {
            closeAlipayOrder(context);
            return;
        }
        if (isWechatChannel(context.channel.getCode())) {
            closeWechatOrder(context);
            return;
        }
        throw new ServiceException("暂不支持的支付渠道: " + context.channel.getCode());
    }

    private PaymentChannelResult queryAlipayOrder(PaymentOrderContext context) throws Exception {
        AlipayChannelConfig config = parseAlipayConfig(context.channel.getConfig());
        Map<String, Object> bizContent = Map.of("out_trade_no", context.extension.getNo());
        String response = invokeAlipayOpenApi(config, "alipay.trade.query", bizContent, context.payOrder.getNotifyUrl(), context.payOrder.getReturnUrl());
        JsonNode root = JsonUtils.getObjectMapper().readTree(response);
        JsonNode node = root.path("alipay_trade_query_response");
        String code = node.path("code").asText();
        if (!"10000".equals(code) && !"40004".equals(code)) {
            throw new ServiceException("支付宝查单失败: " + node.path("sub_msg").asText(node.path("msg").asText()));
        }
        String tradeStatus = node.path("trade_status").asText();
        PaymentChannelResult result = new PaymentChannelResult();
        result.rawData = response;
        result.thirdTradeNo = node.path("trade_no").asText(null);
        result.channelUserId = node.path("buyer_user_id").asText(null);
        // 支付宝返回 40004(交易不存在) 在刚下单阶段可能是暂态，不应直接本地关单；
        // 对于超时单会在过期补偿分支里继续执行关单（且关单接口对不存在交易已做幂等处理）。
        if ("40004".equals(code)) {
            return result;
        }
        result.success = "TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus);
        result.closed = "TRADE_CLOSED".equals(tradeStatus);
        String paidTime = node.path("send_pay_date").asText(node.path("gmt_payment").asText());
        if (StrUtil.isNotBlank(paidTime)) {
            LocalDateTime localDateTime = LocalDateTime.parse(paidTime, ALIPAY_TIME_FORMATTER);
            result.paidTime = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
        }
        return result;
    }

    private void closeAlipayOrder(PaymentOrderContext context) {
        AlipayChannelConfig config = parseAlipayConfig(context.channel.getConfig());
        Map<String, Object> bizContent = Map.of("out_trade_no", context.extension.getNo());
        String response = invokeAlipayOpenApi(config, "alipay.trade.close", bizContent, context.payOrder.getNotifyUrl(), context.payOrder.getReturnUrl());
        try {
            JsonNode root = JsonUtils.getObjectMapper().readTree(response);
            JsonNode node = root.path("alipay_trade_close_response");
            String code = node.path("code").asText();
            String subCode = node.path("sub_code").asText();
            // 交易不存在时无需再关单，视为幂等成功，避免补偿任务持续报错
            if ("40004".equals(code) && "ACQ.TRADE_NOT_EXIST".equalsIgnoreCase(subCode)) {
                return;
            }
            if (!"10000".equals(code)) {
                throw new ServiceException("支付宝关单失败: " + node.path("sub_msg").asText(node.path("msg").asText()));
            }
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new ServiceException("解析支付宝关单响应失败: " + e.getMessage());
        }
    }

    private PaymentChannelResult queryWechatOrder(PaymentOrderContext context) throws Exception {
        WechatChannelConfig config = parseWechatConfig(context.channel.getConfig());
        String path = "/v3/pay/transactions/out-trade-no/" + context.extension.getNo() + "?mchid=" + config.getMerchantId();
        String response = invokeWechatApi(config, "GET", path, "");
        JsonNode node = JsonUtils.getObjectMapper().readTree(response);
        PaymentChannelResult result = new PaymentChannelResult();
        result.rawData = response;
        result.thirdTradeNo = node.path("transaction_id").asText(null);
        result.channelUserId = node.path("payer").path("openid").asText(null);
        String tradeState = node.path("trade_state").asText();
        result.success = "SUCCESS".equals(tradeState);
        result.closed = "CLOSED".equals(tradeState) || "PAYERROR".equals(tradeState) || "REVOKED".equals(tradeState);
        String successTime = node.path("success_time").asText();
        if (StrUtil.isNotBlank(successTime)) {
            result.paidTime = Date.from(OffsetDateTime.parse(successTime).toInstant());
        }
        return result;
    }

    private void closeWechatOrder(PaymentOrderContext context) {
        WechatChannelConfig config = parseWechatConfig(context.channel.getConfig());
        String path = "/v3/pay/transactions/out-trade-no/" + context.extension.getNo() + "/close";
        String body = JsonUtils.toJsonString(Map.of("mchid", config.getMerchantId()));
        invokeWechatApi(config, "POST", path, body);
    }

    @Transactional(rollbackFor = Exception.class)
    protected void markOrderPaid(String extensionNo, String thirdTradeNo, String payerId, String notifyData, Date paidTime) {
        PayOrderExtension extension = payOrderExtensionMapper.selectOne(Wrappers.<PayOrderExtension>lambdaQuery()
                .eq(PayOrderExtension::getNo, extensionNo)
                .last("limit 1"));
        if (extension == null) {
            throw new ServiceException("支付拓展单不存在: " + extensionNo);
        }
        if (Objects.equals(extension.getStatus(), PayOrderStatusEnum.SUCCESS.getStatus())) {
            return;
        }
        PayOrder payOrder = payOrderMapper.selectById(extension.getOrderId());
        if (payOrder == null) {
            throw new ServiceException("支付订单不存在");
        }
        PayRechargeOrder rechargeOrder = payRechargeOrderMapper.selectById(payOrder.getBizOrderId());
        if (rechargeOrder == null) {
            throw new ServiceException("充值订单不存在");
        }

        extension.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
        extension.setChannelNotifyData(truncate(notifyData));
        payOrderExtensionMapper.updateById(extension);

        payOrder.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
        payOrder.setSuccessTime(paidTime);
        payOrder.setExtensionId(extension.getId());
        payOrder.setChannelId(extension.getChannelId());
        payOrder.setChannelCode(extension.getChannelCode());
        payOrder.setChannelUserId(StrUtil.blankToDefault(payerId, payOrder.getChannelUserId()));
        payOrder.setChannelOrderNo(thirdTradeNo);
        payOrderMapper.updateById(payOrder);

        if (!Objects.equals(rechargeOrder.getPayStatus(), PayOrderStatusEnum.SUCCESS.getStatus())) {
            rechargeOrder.setPayStatus(PayOrderStatusEnum.SUCCESS.getStatus());
            rechargeOrder.setThirdTradeNo(thirdTradeNo);
            rechargeOrder.setPaidTime(paidTime);
            payRechargeOrderMapper.updateById(rechargeOrder);
            increaseWalletBalance(rechargeOrder.getAccountId(), rechargeOrder.getRechargeAmount(), rechargeOrder.getRechargeOrderNo());
        }
    }

    @Transactional(rollbackFor = Exception.class)
    protected void markOrderClosed(String extensionNo, String thirdTradeNo, String notifyData) {
        if (StrUtil.isBlank(extensionNo)) {
            return;
        }
        PayOrderExtension extension = payOrderExtensionMapper.selectOne(Wrappers.<PayOrderExtension>lambdaQuery()
                .eq(PayOrderExtension::getNo, extensionNo)
                .last("limit 1"));
        if (extension == null || Objects.equals(extension.getStatus(), PayOrderStatusEnum.SUCCESS.getStatus())) {
            return;
        }
        extension.setStatus(PayOrderStatusEnum.CLOSED.getStatus());
        extension.setChannelNotifyData(truncate(notifyData));
        payOrderExtensionMapper.updateById(extension);

        PayOrder payOrder = payOrderMapper.selectById(extension.getOrderId());
        if (payOrder != null && !Objects.equals(payOrder.getStatus(), PayOrderStatusEnum.SUCCESS.getStatus())) {
            payOrder.setStatus(PayOrderStatusEnum.CLOSED.getStatus());
            payOrder.setChannelOrderNo(thirdTradeNo);
            payOrderMapper.updateById(payOrder);
        }
        if (payOrder != null && Objects.equals(payOrder.getBizOrderType(), PayOrderBizTypeEnum.FUND_RECHARGE.getCode())) {
            PayRechargeOrder rechargeOrder = payRechargeOrderMapper.selectById(payOrder.getBizOrderId());
            if (rechargeOrder != null && !Objects.equals(rechargeOrder.getPayStatus(), PayOrderStatusEnum.SUCCESS.getStatus())) {
                rechargeOrder.setPayStatus(PayOrderStatusEnum.CLOSED.getStatus());
                rechargeOrder.setThirdTradeNo(thirdTradeNo);
                payRechargeOrderMapper.updateById(rechargeOrder);
            }
        }
    }

    private PayWallet getOrCreateWallet(Long companyId, Long userId, Long walletType) {
        Long actualWalletType = walletType == null ? 2L : walletType;
        Long walletUserId = resolveWalletUserId(companyId, userId, actualWalletType);
        LambdaQueryWrapper<PayWallet> wrapper = Wrappers.<PayWallet>lambdaQuery()
                .eq(PayWallet::getUserId, walletUserId)
                .eq(PayWallet::getWalletType, actualWalletType)
                .last("limit 1");
        PayWallet wallet = payWalletMapper.selectOne(wrapper);
        if (wallet != null) {
            return wallet;
        }
        wallet = new PayWallet();
        wallet.setUserId(walletUserId);
        wallet.setWalletType(actualWalletType);
        wallet.setBalance(BigDecimal.ZERO);
        wallet.setTotalExpense(BigDecimal.ZERO);
        wallet.setTotalRecharge(BigDecimal.ZERO);
        wallet.setFreezePrice(BigDecimal.ZERO);
        wallet.setFreezeStatus(WALLET_FREEZE_STATUS_NORMAL);
        payWalletMapper.insert(wallet);
        return wallet;
    }

    private Long resolveWalletUserId(Long companyId, Long userId, Long walletType) {
        return Objects.equals(walletType, 2L) ? companyId : userId;
    }

    private void checkDuplicatePendingRechargeOrder(Long userId, Long walletId) {
        PayRechargeOrder latestRechargeOrder = payRechargeOrderMapper.selectOne(Wrappers.<PayRechargeOrder>lambdaQuery()
                .eq(PayRechargeOrder::getUserId, userId)
                .eq(PayRechargeOrder::getAccountId, walletId)
                .in(PayRechargeOrder::getPayStatus, PayOrderStatusEnum.WAITING.getStatus(), PayOrderStatusEnum.PAYING.getStatus())
                .orderByDesc(PayRechargeOrder::getId)
                .last("limit 1"));
        if (latestRechargeOrder == null) {
            return;
        }
        Date now = new Date();
        PayOrder pendingPayOrder = payOrderMapper.selectOne(Wrappers.<PayOrder>lambdaQuery()
                .eq(PayOrder::getBizOrderType, PayOrderBizTypeEnum.FUND_RECHARGE.getCode())
                .eq(PayOrder::getBizOrderId, latestRechargeOrder.getId())
                .in(PayOrder::getStatus, PayOrderStatusEnum.WAITING.getStatus(), PayOrderStatusEnum.PAYING.getStatus())
                .and(wrapper -> wrapper.isNull(PayOrder::getExpireTime).or().gt(PayOrder::getExpireTime, now))
                .orderByDesc(PayOrder::getId)
                .last("limit 1"));
        if (pendingPayOrder != null) {
            throw new ServiceException("存在待支付充值订单，请先完成支付（订单号：" + latestRechargeOrder.getRechargeOrderNo() + "）");
        }
    }

    private void increaseWalletBalance(Long walletId, BigDecimal amount, String bizId) {
        PayWallet wallet = payWalletMapper.selectById(walletId);
        if (wallet == null) {
            throw new ServiceException("钱包不存在");
        }
        ensureWalletAvailable(wallet, "钱包已冻结，无法充值");
        BigDecimal balance = Objects.requireNonNullElse(wallet.getBalance(), BigDecimal.ZERO);
        BigDecimal totalRecharge = Objects.requireNonNullElse(wallet.getTotalRecharge(), BigDecimal.ZERO);
        wallet.setBalance(balance.add(amount));
        wallet.setTotalRecharge(totalRecharge.add(amount));
        if (payWalletMapper.updateById(wallet) <= 0) {
            throw new ServiceException("更新钱包余额失败，请重试");
        }

        PayWalletTransaction transaction = new PayWalletTransaction();
        transaction.setWalletId(walletId);
        transaction.setBizType(WALLET_BIZ_TYPE_RECHARGE);
        transaction.setBizId(bizId);
        transaction.setNo(buildNo("WT"));
        transaction.setTitle("钱包充值");
        transaction.setPrice(amount);
        transaction.setBalance(wallet.getBalance());
        if (payWalletTransactionMapper.insert(transaction) <= 0) {
            throw new ServiceException("写入钱包流水失败，请重试");
        }
    }

    private void ensureWalletAvailable(PayWallet wallet, String message) {
        if (wallet != null && Objects.equals(wallet.getFreezeStatus(), WALLET_FREEZE_STATUS_FROZEN)) {
            throw new ServiceException(message);
        }
    }

    private PayApp getActivePayApp() {
        PayApp app = payAppMapper.selectOne(Wrappers.<PayApp>lambdaQuery()
                .eq(PayApp::getAppKey, DEFAULT_PAY_APP_KEY)
                .eq(PayApp::getStatus, 1L)
                .last("limit 1"));
        if (app != null) {
            return app;
        }
        app = payAppMapper.selectOne(Wrappers.<PayApp>lambdaQuery()
                .eq(PayApp::getStatus, 1L)
                .orderByAsc(PayApp::getId)
                .last("limit 1"));
        if (app == null) {
            throw new ServiceException("未配置可用的支付应用，请先维护 pay_app");
        }
        return app;
    }

    private PayChannel getActiveChannel(Long appId, String channelCode) {
        PayChannel channel = payChannelMapper.selectOne(Wrappers.<PayChannel>lambdaQuery()
                .eq(PayChannel::getAppId, appId)
                .eq(PayChannel::getCode, channelCode)
                .eq(PayChannel::getStatus, 1L)
                .last("limit 1"));
        if (channel == null) {
            throw new ServiceException("支付渠道未配置或未启用: " + channelCode);
        }
        return channel;
    }

    private PayChannel getChannelById(Long channelId) {
        PayChannel channel = payChannelMapper.selectById(channelId);
        if (channel == null) {
            throw new ServiceException("支付渠道不存在");
        }
        return channel;
    }

    private AlipayChannelConfig parseAlipayConfig(String configJson) {
        AlipayChannelConfig config = JsonUtils.parseObject(configJson, AlipayChannelConfig.class);
        if (config == null || StrUtil.hasBlank(config.getAppId(), config.getPrivateKey(), config.getAlipayPublicKey())) {
            throw new ServiceException("支付宝渠道配置不完整");
        }
        return config;
    }

    private WechatChannelConfig parseWechatConfig(String configJson) {
        WechatChannelConfig config = JsonUtils.parseObject(configJson, WechatChannelConfig.class);
        if (config == null || StrUtil.hasBlank(config.getAppId(), config.getMerchantId(), config.getMerchantSerialNo(),
                config.getPrivateKey(), config.getApiV3Key(), config.getPlatformPublicKey())) {
            throw new ServiceException("微信渠道配置不完整");
        }
        return config;
    }

    private String buildNotifyUrl(HttpServletRequest request, String provider, Long channelId) {
        StringBuilder builder = new StringBuilder();
        builder.append(request.getScheme()).append("://").append(request.getServerName());
        if (request.getServerPort() != 80 && request.getServerPort() != 443) {
            builder.append(":").append(request.getServerPort());
        }
        builder.append("/pay/open/notify/")
                .append(provider.toLowerCase())
                .append("/")
                .append(channelId);
        return builder.toString();
    }

    private String buildPlainQuery(Map<String, String> params, boolean urlEncode) {
        return params.entrySet().stream()
                .filter(entry -> StrUtil.isNotBlank(entry.getValue()))
                .sorted(Comparator.comparing(Map.Entry::getKey))
                .map(entry -> entry.getKey() + "=" + (urlEncode ? urlEncode(entry.getValue()) : entry.getValue()))
                .collect(Collectors.joining("&"));
    }

    private String urlEncode(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }

    private BigDecimal normalizeAmount(BigDecimal amount) {
        return amount.setScale(2, RoundingMode.HALF_UP);
    }

    private int toFen(BigDecimal amount) {
        return normalizeAmount(amount).multiply(BigDecimal.valueOf(100)).intValueExact();
    }

    private String buildNo(String prefix) {
        return prefix + DateUtil.format(new Date(), "yyyyMMddHHmmss") + StrUtil.sub(IdUtil.fastSimpleUUID(), 0, 10).toUpperCase();
    }

    private String truncate(String text) {
        if (text == null) {
            return null;
        }
        return text.length() > 2000 ? text.substring(0, 2000) : text;
    }

    private String defaultIfBlank(String value, String defaultValue) {
        return StrUtil.isBlank(value) ? defaultValue : value;
    }

    private String buildChannelDisplayName(PayChannelCodeEnum channelCodeEnum) {
        String providerName = "ALIPAY".equalsIgnoreCase(channelCodeEnum.getProvider()) ? "支付宝" : "微信";
        String sceneName = switch (channelCodeEnum.getScene().toUpperCase()) {
            case "QR" -> "扫码";
            case "H5" -> "H5";
            case "APP" -> "APP";
            default -> channelCodeEnum.getScene();
        };
        return providerName + sceneName;
    }

    private boolean isAlipayChannel(String channelCode) {
        return StrUtil.startWithIgnoreCase(channelCode, "ALIPAY");
    }

    private boolean isWechatChannel(String channelCode) {
        return StrUtil.startWithIgnoreCase(channelCode, "WECHAT");
    }

    private String toOffsetDateTime(Date date) {
        return date.toInstant().atOffset(ZoneOffset.ofHours(8)).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
    }

    private record PaymentOrderContext(PayOrder payOrder, PayOrderExtension extension, PayChannel channel) {
    }

    private static class PaymentChannelResult {
        private boolean success;
        private boolean closed;
        private String thirdTradeNo;
        private String channelUserId;
        private String rawData;
        private Date paidTime;

        private boolean isSuccess() {
            return success;
        }

        private boolean isClosed() {
            return closed;
        }
    }

    @Data
    private static class AlipayChannelConfig {
        private String appId;
        private String privateKey;
        private String alipayPublicKey;
        private String gateway;
    }

    @Data
    private static class WechatChannelConfig {
        private String appId;
        private String merchantId;
        private String merchantSerialNo;
        private String privateKey;
        private String apiV3Key;
        private String platformPublicKey;
        private String gateway;
    }
}
