package controller

import (
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"math"
	"net/http"
	"net/url"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/QuantumNous/new-api/common"
	"github.com/QuantumNous/new-api/logger"
	"github.com/QuantumNous/new-api/model"
	"github.com/QuantumNous/new-api/service"
	"github.com/QuantumNous/new-api/setting"
	"github.com/QuantumNous/new-api/setting/operation_setting"
	"github.com/QuantumNous/new-api/setting/system_setting"

	"github.com/Calcium-Ion/go-epay/epay"
	"github.com/gin-gonic/gin"
	"github.com/samber/lo"
	"github.com/shopspring/decimal"
)

func GetTopUpInfo(c *gin.Context) {
	// 获取支付方式
	payMethods := operation_setting.PayMethods

	// 如果启用了 Stripe 支付，添加到支付方法列表
	if setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "" {
		// 检查是否已经包含 Stripe
		hasStripe := false
		for _, method := range payMethods {
			if method["type"] == "stripe" {
				hasStripe = true
				break
			}
		}

		if !hasStripe {
			stripeMethod := map[string]string{
				"name":      "Stripe",
				"type":      "stripe",
				"color":     "rgba(var(--semi-purple-5), 1)",
				"min_topup": strconv.Itoa(setting.StripeMinTopUp),
			}
			payMethods = append(payMethods, stripeMethod)
		}
	}

	// 如果启用了 Waffo 支付，添加到支付方法列表
	enableWaffo := setting.WaffoEnabled &&
		((!setting.WaffoSandbox &&
			setting.WaffoApiKey != "" &&
			setting.WaffoPrivateKey != "" &&
			setting.WaffoPublicCert != "") ||
			(setting.WaffoSandbox &&
				setting.WaffoSandboxApiKey != "" &&
				setting.WaffoSandboxPrivateKey != "" &&
				setting.WaffoSandboxPublicCert != ""))
	if enableWaffo {
		hasWaffo := false
		for _, method := range payMethods {
			if method["type"] == "waffo" {
				hasWaffo = true
				break
			}
		}

		if !hasWaffo {
			waffoMethod := map[string]string{
				"name":      "Waffo (Global Payment)",
				"type":      "waffo",
				"color":     "rgba(var(--semi-blue-5), 1)",
				"min_topup": strconv.Itoa(setting.WaffoMinTopUp),
			}
			payMethods = append(payMethods, waffoMethod)
		}
	}

	data := gin.H{
		"enable_online_topup": (operation_setting.OnlinePayProvider == "yipay" &&
			(operation_setting.YipayRequestURL != "" || operation_setting.PayAddress != "") &&
			operation_setting.YipayMchNo != "" &&
			operation_setting.YipayAppId != "" &&
			operation_setting.YipayAppSecret != "") ||
			(operation_setting.OnlinePayProvider != "yipay" &&
				operation_setting.PayAddress != "" &&
				operation_setting.EpayId != "" &&
				operation_setting.EpayKey != ""),
		"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
		"enable_creem_topup":  setting.CreemApiKey != "" && setting.CreemProducts != "[]",
		"enable_waffo_topup":  enableWaffo,
		"waffo_pay_methods": func() interface{} {
			if enableWaffo {
				return setting.GetWaffoPayMethods()
			}
			return nil
		}(),
		"creem_products":      setting.CreemProducts,
		"pay_methods":         payMethods,
		"min_topup":           operation_setting.MinTopUp,
		"stripe_min_topup":    setting.StripeMinTopUp,
		"waffo_min_topup":     setting.WaffoMinTopUp,
		"amount_options":      operation_setting.GetPaymentSetting().AmountOptions,
		"discount":            operation_setting.GetPaymentSetting().AmountDiscount,
		"online_pay_provider": operation_setting.OnlinePayProvider,
	}
	common.ApiSuccess(c, data)
}

type EpayRequest struct {
	Amount        int64  `json:"amount"`
	PaymentMethod string `json:"payment_method"`
}

type AmountRequest struct {
	Amount int64 `json:"amount"`
}

// buildUserEpayNotifyURL 规范化用户充值异步回调地址，避免重复拼接 notify 路径。
func buildUserEpayNotifyURL(callbackAddress string) string {
	normalized := strings.TrimRight(strings.TrimSpace(callbackAddress), "/")
	if strings.HasSuffix(normalized, "/api/user/epay/notify") {
		return normalized
	}
	return normalized + "/api/user/epay/notify"
}

// verifyYipayNotify 验证 Yipay 回调签名。
func verifyYipayNotify(params map[string]string) bool {
	sign := strings.TrimSpace(params["sign"])
	if sign == "" || operation_setting.YipayAppSecret == "" {
		return false
	}
	expected := signYipayMD5(params, operation_setting.YipayAppSecret)
	return strings.EqualFold(sign, expected)
}

// sortedParamKeys 返回回调参数的有序键列表，便于日志排查。
func sortedParamKeys(params map[string]string) []string {
	keys := make([]string, 0, len(params))
	for k := range params {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}

// signYipayMD5 生成 Yipay MD5 签名（参数按 ASCII 排序）。
func signYipayMD5(params map[string]string, appSecret string) string {
	keys := make([]string, 0, len(params))
	for k, v := range params {
		if k == "sign" || strings.TrimSpace(v) == "" {
			continue
		}
		keys = append(keys, k)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys)+1)
	for _, k := range keys {
		parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
	}
	parts = append(parts, "key="+appSecret)
	raw := strings.Join(parts, "&")
	sum := md5.Sum([]byte(raw))
	return strings.ToUpper(hex.EncodeToString(sum[:]))
}

// parseYipayUnifiedOrderData 解析 Jeepay/Yipay 统一下单响应中的 data 字段（文档多为 JSON 对象，部分网关序列化为 JSON 字符串）。
func parseYipayUnifiedOrderData(yipayResp map[string]any) (map[string]any, error) {
	raw, ok := yipayResp["data"]
	if !ok || raw == nil {
		return nil, fmt.Errorf("响应缺少 data 字段")
	}
	if m, ok := raw.(map[string]any); ok {
		return m, nil
	}
	s, ok := raw.(string)
	if !ok {
		return nil, fmt.Errorf("data 字段类型不支持")
	}
	s = strings.TrimSpace(s)
	if s == "" || s == "{}" {
		return nil, fmt.Errorf("data 字段为空")
	}
	var m map[string]any
	if err := json.Unmarshal([]byte(s), &m); err != nil {
		return nil, fmt.Errorf("data JSON 字符串解析失败: %w", err)
	}
	return m, nil
}

// yipayOrderStringField 从统一下单 data 中读取字符串字段（兼容驼峰与蛇形命名，忽略空值与占位 "<nil>"）。
func yipayOrderStringField(m map[string]any, keys ...string) string {
	for _, k := range keys {
		v, ok := m[k]
		if !ok || v == nil {
			continue
		}
		s := strings.TrimSpace(fmt.Sprintf("%v", v))
		if s == "" || s == "<nil>" {
			continue
		}
		return s
	}
	return ""
}

// looksLikeYipayPayJumpURL 判断是否为网关返回的可拉起收银台的链接（http(s)、微信/支付宝 App scheme 等）。
func looksLikeYipayPayJumpURL(s string) bool {
	low := strings.ToLower(strings.TrimSpace(s))
	if strings.HasPrefix(low, "http://") || strings.HasPrefix(low, "https://") {
		return true
	}
	if strings.HasPrefix(low, "weixin://") || strings.HasPrefix(low, "weixinpay://") {
		return true
	}
	if strings.HasPrefix(low, "alipays://") || strings.HasPrefix(low, "alipay://") {
		return true
	}
	return false
}

// yipayURLExtractMaxDepth 限制嵌套 map/JSON 解析深度，防止异常响应导致过深递归。
const yipayURLExtractMaxDepth = 8

// yipayPayLinkInTextRegexp 从文本中提取首个支付相关链接（http(s)、weixin://、alipay(s)://）。
var yipayPayLinkInTextRegexp = regexp.MustCompile(`(?:https?://|weixin://[^\s"'<>]*|weixinpay://[^\s"'<>]*|alipay?s://[^\s"'<>]+)`)

// yipayFirstPayLinkInString 返回文本中出现的第一个可识别的支付跳转链接。
func yipayFirstPayLinkInString(s string) string {
	m := yipayPayLinkInTextRegexp.FindString(s)
	return strings.TrimSpace(m)
}

// yipaySortedMapKeys 返回 map 键的有序列表，便于日志排查。
func yipaySortedMapKeys(m map[string]any) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}

// yipayCoerceValueToHTTPURL 将网关返回字段转为可拉起支付的链接（http(s)、weixin:// 等；string / JSON / 嵌套 map / 文本内嵌）。
func yipayCoerceValueToHTTPURL(v any, depth int) string {
	if v == nil || depth > yipayURLExtractMaxDepth {
		return ""
	}
	switch t := v.(type) {
	case string:
		s := strings.TrimSpace(t)
		if s == "" || s == "<nil>" {
			return ""
		}
		if looksLikeYipayPayJumpURL(s) {
			return s
		}
		st := strings.TrimSpace(s)
		if strings.HasPrefix(st, "{") || strings.HasPrefix(st, "[") {
			var raw json.RawMessage
			if err := json.Unmarshal([]byte(s), &raw); err == nil {
				var obj map[string]any
				if err := json.Unmarshal(raw, &obj); err == nil {
					if u := yipayExtractHTTPURLFromMap(obj, depth+1); u != "" {
						return u
					}
				}
			}
		}
		if u := yipayFirstPayLinkInString(s); u != "" {
			return u
		}
		return ""
	case map[string]any:
		return yipayExtractHTTPURLFromMap(t, depth+1)
	case []any:
		for _, item := range t {
			if u := yipayCoerceValueToHTTPURL(item, depth+1); u != "" {
				return u
			}
		}
		return ""
	default:
		s := strings.TrimSpace(fmt.Sprintf("%v", t))
		if looksLikeYipayPayJumpURL(s) {
			return s
		}
		return ""
	}
}

// yipayExtractHTTPURLFromMap 在 map 中按常见字段名优先查找支付链接，再浅层扫描其余字段（跳过错误信息类键）。
func yipayExtractHTTPURLFromMap(m map[string]any, depth int) string {
	if m == nil || depth > yipayURLExtractMaxDepth {
		return ""
	}
	priorityKeys := []string{
		"payUrl", "pay_url", "payJumpUrl", "pay_jump_url",
		"codeUrl", "code_url",
		"codeImgUrl", "code_img_url",
		"url", "link", "h5Url", "h5_url", "checkoutUrl", "checkout_url",
		"approvalUrl", "approval_url", "redirectUrl", "redirect_url",
		"mweb_url", "mwebUrl", "payData", "pay_data",
	}
	for _, k := range priorityKeys {
		if u := yipayCoerceValueToHTTPURL(m[k], depth+1); u != "" {
			return u
		}
	}
	skipKeys := map[string]struct{}{
		"errMsg": {}, "err_msg": {}, "errCode": {}, "err_code": {},
		"channelErrMsg": {}, "channelErrCode": {}, "errMsgFromChannel": {},
	}
	for k, v := range m {
		if _, skip := skipKeys[k]; skip {
			continue
		}
		if u := yipayCoerceValueToHTTPURL(v, depth+1); u != "" {
			return u
		}
	}
	return ""
}

// extractYipayPayURLFromOrderData 从 data 中提取支付跳转 URL（兼容对象型 payData、嵌套 JSON、非标准字段名；表单收银台单独提示）。
func extractYipayPayURLFromOrderData(dataObj map[string]any) (string, error) {
	payDataType := strings.ToLower(yipayOrderStringField(dataObj, "payDataType", "pay_data_type"))
	payDataStr := yipayOrderStringField(dataObj, "payData", "pay_data")
	payDataRaw := dataObj["payData"]
	if payDataRaw == nil {
		payDataRaw = dataObj["pay_data"]
	}

	if payDataType == "form" || strings.Contains(strings.ToLower(payDataStr), "<form") {
		for _, k := range []string{"payUrl", "pay_url", "payJumpUrl", "pay_jump_url"} {
			if u := yipayCoerceValueToHTTPURL(dataObj[k], 0); u != "" {
				return u, nil
			}
		}
		return "", fmt.Errorf("Yipay 返回表单收银台（payDataType=form），当前充值页仅支持链接跳转；请在网关配置返回 payUrl/payJumpUrl，或升级前端支持表单 HTML 收银台")
	}

	for _, key := range []string{"payUrl", "pay_url", "payJumpUrl", "pay_jump_url"} {
		if u := yipayCoerceValueToHTTPURL(dataObj[key], 0); u != "" {
			return u, nil
		}
	}
	if u := yipayCoerceValueToHTTPURL(payDataRaw, 0); u != "" {
		return u, nil
	}
	if u := yipayExtractHTTPURLFromMap(dataObj, 0); u != "" {
		return u, nil
	}

	ec := yipayOrderStringField(dataObj, "errCode", "err_code")
	em := yipayOrderStringField(dataObj, "errMsg", "err_msg")
	os := yipayOrderStringField(dataObj, "orderState", "order_state")
	pid := yipayOrderStringField(dataObj, "payOrderId", "pay_order_id")
	common.SysLog(fmt.Sprintf(
		"[Yipay] unifiedOrder data 未解析出跳转链接: keys=%v payDataType=%q payDataGoType=%T errCode=%q errMsg=%q orderState=%s payOrderId=%s",
		yipaySortedMapKeys(dataObj), payDataType, payDataRaw, ec, em, os, pid,
	))
	if ec != "" || em != "" {
		return "", fmt.Errorf("Jeepay 已创建订单 payOrderId=%s（orderState=%s），上游 errCode=%q errMsg=%q 且无跳转链接；请核对 PayPal 通道配置，或在管理后台「Yipay 渠道扩展参数」填写 {\"payDataType\":\"payUrl\"}", pid, os, ec, em)
	}
	if pid != "" {
		return "", fmt.Errorf("Jeepay 返回 payOrderId=%s、orderState=%s，但 data 中未解析出可用支付链接（微信支付多为 codeUrl/weixin://；H5 多为 mweb_url；PayPal 多为 payUrl；PP_PC 需在 channelExtra 提供 cancelUrl 等）", pid, os)
	}
	return "", fmt.Errorf("Yipay 返回支付链接为空（data 内无可用 http(s) 链接，详见服务端日志）")
}

// yipayJeepayDefaultChannelExtra 构造 yiPay/Jeepay 默认 channelExtra。
// PP_PC：UnifiedOrderRQ.buildBizRQ 将 channelExtra 解析为 PPPcOrderRQ，其中 cancelUrl 为 @NotBlank（见 yiPay PpPc.java / PPPcOrderRQ.java）。
func yipayJeepayDefaultChannelExtra(wayCode string, returnURL string) string {
	u := strings.ToUpper(strings.TrimSpace(wayCode))
	if u == "" {
		return ""
	}
	ret := strings.TrimSpace(returnURL)
	if u == "PP_PC" {
		if ret == "" {
			ret = "http://127.0.0.1/console/log"
		}
		b, err := json.Marshal(map[string]string{
			"payDataType": "payUrl",
			"cancelUrl":   ret,
		})
		if err != nil {
			return ""
		}
		return string(b)
	}
	if strings.HasSuffix(u, "_PC") {
		return `{"payDataType":"payUrl"}`
	}
	return ""
}

// mergeYipayJeepayChannelExtra 合并管理端配置的 channelExtra JSON 与 wayCode 默认值（仅补充对方 JSON 中未出现的键）。
func mergeYipayJeepayChannelExtra(userJSON string, wayCode string, returnURL string) string {
	userJSON = strings.TrimSpace(userJSON)
	def := yipayJeepayDefaultChannelExtra(wayCode, returnURL)
	if userJSON == "" {
		return def
	}
	if def == "" {
		return userJSON
	}
	var u map[string]any
	if err := json.Unmarshal([]byte(userJSON), &u); err != nil {
		return userJSON
	}
	var d map[string]any
	if err := json.Unmarshal([]byte(def), &d); err != nil {
		return userJSON
	}
	for k, v := range d {
		if _, exists := u[k]; !exists {
			u[k] = v
		}
	}
	out, err := json.Marshal(u)
	if err != nil {
		return userJSON
	}
	return string(out)
}

// isPayPalJeepayWayCode 判断是否为 yiPay/Jeepay 的 PayPal 产品码（如 PP_PC）；PayPal REST 常不支持以 CNY 下单。
func isPayPalJeepayWayCode(wayCode string) bool {
	u := strings.ToUpper(strings.TrimSpace(wayCode))
	return strings.HasPrefix(u, "PP_")
}

// yipayJeepayAmountMinorAndCurrency 计算统一下单的 amount（最小货币单位）与币种。
// payMoney 与 getPayMoney 一致，单位为人民币元；PayPal 通道换算为 USD 美分（汇率优先 USDExchangeRate，其次 Price）。
func yipayJeepayAmountMinorAndCurrency(payMoney float64, wayCode string) (minor int64, currency string) {
	currency = "cny"
	minor = int64(math.Round(payMoney * 100))
	if minor < 1 {
		minor = 1
	}
	if !isPayPalJeepayWayCode(wayCode) {
		return minor, currency
	}
	rate := operation_setting.USDExchangeRate
	if rate <= 0 {
		rate = operation_setting.Price
	}
	if rate <= 0 {
		rate = 7.3
	}
	usd := payMoney / rate
	minor = int64(math.Round(usd * 100))
	if minor < 1 {
		minor = 1
	}
	return minor, "usd"
}

// requestYipayOrder 按 Yipay OpenAPI 创建统一下单请求。
func requestYipayOrder(req EpayRequest, id int, payMoney float64, paymentMethod string) (string, map[string]string, error) {
	requestURL := operation_setting.YipayRequestURL
	if requestURL == "" {
		requestURL = operation_setting.PayAddress
	}
	requestURL = strings.TrimRight(requestURL, "/")
	if requestURL == "" {
		return "", nil, fmt.Errorf("未配置 Yipay 请求地址")
	}
	if operation_setting.YipayMchNo == "" || operation_setting.YipayAppId == "" || operation_setting.YipayAppSecret == "" {
		return "", nil, fmt.Errorf("未配置完整的 Yipay 参数")
	}
	callBackAddress := service.GetCallbackAddress()
	returnURLString := system_setting.ServerAddress + "/console/log"
	if operation_setting.YipayReturnUrl != "" {
		returnURLString = operation_setting.YipayReturnUrl
	}
	notifyURLString := buildUserEpayNotifyURL(callBackAddress)
	if operation_setting.YipayNotifyUrl != "" {
		notifyURLString = operation_setting.YipayNotifyUrl
	}
	tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
	tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
	amountMinor, orderCurrency := yipayJeepayAmountMinorAndCurrency(payMoney, paymentMethod)
	reqTime := strconv.FormatInt(time.Now().UnixMilli(), 10)
	chExtra := mergeYipayJeepayChannelExtra(strings.TrimSpace(operation_setting.YipayChannelExtra), paymentMethod, returnURLString)
	params := map[string]string{
		"mchNo":      operation_setting.YipayMchNo,
		"appId":      operation_setting.YipayAppId,
		"mchOrderNo": tradeNo,
		"wayCode":    paymentMethod,
		"amount":     strconv.FormatInt(amountMinor, 10),
		"currency":   strings.ToLower(orderCurrency),
		"clientIp":   "127.0.0.1",
		"subject":    fmt.Sprintf("TUC%d", req.Amount),
		"body":       fmt.Sprintf("TUC%d", req.Amount),
		"notifyUrl":  notifyURLString,
		"returnUrl":  returnURLString,
		"reqTime":    reqTime,
		"version":    "1.0",
		"signType":   "MD5",
	}
	if chExtra != "" {
		params["channelExtra"] = chExtra
	}
	// 调试日志：输出关键参数的可见信息，便于定位是否包含前后空格（不输出 AppSecret 明文）。
	common.SysLog(fmt.Sprintf(
		"[Yipay] unifiedOrder params: mchNo=%q(len=%d,trimChanged=%t), appId=%q(len=%d,trimChanged=%t), wayCode=%q, amountMinor=%s currency=%s payMoneyCNY=%.4f channelExtra=%q, notifyUrl=%q, returnUrl=%q, reqTime=%q",
		params["mchNo"], len(params["mchNo"]), strings.TrimSpace(params["mchNo"]) != params["mchNo"],
		params["appId"], len(params["appId"]), strings.TrimSpace(params["appId"]) != params["appId"],
		params["wayCode"], params["amount"], params["currency"], payMoney, chExtra, params["notifyUrl"], params["returnUrl"], params["reqTime"],
	))
	params["sign"] = signYipayMD5(params, operation_setting.YipayAppSecret)
	requestJSON, err := common.Marshal(params)
	if err != nil {
		return "", nil, err
	}
	unifiedOrderURL := requestURL
	if !strings.Contains(strings.ToLower(unifiedOrderURL), "/api/pay/unifiedorder") {
		unifiedOrderURL = requestURL + "/api/pay/unifiedOrder"
	}
	httpResp, err := http.Post(unifiedOrderURL, "application/json", strings.NewReader(string(requestJSON)))
	if err != nil {
		return "", nil, err
	}
	defer httpResp.Body.Close()
	respBody, err := io.ReadAll(httpResp.Body)
	if err != nil {
		return "", nil, err
	}
	var yipayResp map[string]any
	if err = common.Unmarshal(respBody, &yipayResp); err != nil {
		respPreview := string(respBody)
		if len(respPreview) > 300 {
			respPreview = respPreview[:300]
		}
		return "", nil, fmt.Errorf("Yipay 响应解析失败(status=%d,url=%s): %s", httpResp.StatusCode, unifiedOrderURL, respPreview)
	}
	code := fmt.Sprintf("%v", yipayResp["code"])
	if code != "0" && code != "200" {
		msg := fmt.Sprintf("%v", yipayResp["msg"])
		if msg == "<nil>" || msg == "" {
			if v, ok := yipayResp["message"]; ok {
				msg = fmt.Sprintf("%v", v)
			}
		}
		if msg == "<nil>" || msg == "" {
			if v, ok := yipayResp["errMsg"]; ok {
				msg = fmt.Sprintf("%v", v)
			}
		}
		if msg == "<nil>" || msg == "" {
			msg = string(respBody)
			if len(msg) > 300 {
				msg = msg[:300]
			}
		}
		return "", nil, fmt.Errorf("Yipay 下单失败(status=%d,code=%s,url=%s): %s", httpResp.StatusCode, code, unifiedOrderURL, msg)
	}
	dataObj, parseErr := parseYipayUnifiedOrderData(yipayResp)
	if parseErr != nil {
		return "", nil, fmt.Errorf("Yipay 统一下单返回解析失败: %w", parseErr)
	}
	// 少数网关/代理把 payUrl、payData 等放在响应根级而非 data 内，合并后再解析。
	for _, k := range []string{"payUrl", "pay_url", "payJumpUrl", "pay_jump_url", "payData", "pay_data", "payDataType", "pay_data_type"} {
		if v, ok := yipayResp[k]; ok && v != nil {
			if _, exists := dataObj[k]; !exists {
				dataObj[k] = v
			}
		}
	}
	payURL, extractErr := extractYipayPayURLFromOrderData(dataObj)
	if extractErr != nil {
		return "", nil, extractErr
	}
	payURL = strings.TrimSpace(payURL)
	amount := req.Amount
	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
		dAmount := decimal.NewFromInt(int64(amount))
		dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
		amount = dAmount.Div(dQuotaPerUnit).IntPart()
	}
	topUp := &model.TopUp{
		UserId:        id,
		Amount:        amount,
		Money:         payMoney,
		TradeNo:       tradeNo,
		PaymentMethod: paymentMethod,
		CreateTime:    time.Now().Unix(),
		Status:        "pending",
	}
	if err = topUp.Insert(); err != nil {
		return "", nil, err
	}
	return payURL, map[string]string{}, nil
}

// GetEpayClient 创建并返回在线充值客户端。
func GetEpayClient() *epay.Client {
	if operation_setting.PayAddress == "" || operation_setting.EpayId == "" || operation_setting.EpayKey == "" {
		return nil
	}
	withUrl, err := epay.NewClient(&epay.Config{
		PartnerID: operation_setting.EpayId,
		Key:       operation_setting.EpayKey,
	}, operation_setting.PayAddress)
	if err != nil {
		return nil
	}
	return withUrl
}

func getPayMoney(amount int64, group string) float64 {
	dAmount := decimal.NewFromInt(amount)
	// 充值金额以“展示类型”为准：
	// - USD/CNY: 前端传 amount 为金额单位；TOKENS: 前端传 tokens，需要换成 USD 金额
	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
		dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
		dAmount = dAmount.Div(dQuotaPerUnit)
	}

	topupGroupRatio := common.GetTopupGroupRatio(group)
	if topupGroupRatio == 0 {
		topupGroupRatio = 1
	}

	dTopupGroupRatio := decimal.NewFromFloat(topupGroupRatio)
	dPrice := decimal.NewFromFloat(operation_setting.Price)
	// apply optional preset discount by the original request amount (if configured), default 1.0
	discount := 1.0
	if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(amount)]; ok {
		if ds > 0 {
			discount = ds
		}
	}
	dDiscount := decimal.NewFromFloat(discount)

	payMoney := dAmount.Mul(dPrice).Mul(dTopupGroupRatio).Mul(dDiscount)

	return payMoney.InexactFloat64()
}

func getMinTopup() int64 {
	minTopup := operation_setting.MinTopUp
	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
		dMinTopup := decimal.NewFromInt(int64(minTopup))
		dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
		minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
	}
	return int64(minTopup)
}

// RequestEpay 创建在线充值订单并拉起支付。
func RequestEpay(c *gin.Context) {
	var req EpayRequest
	err := c.ShouldBindJSON(&req)
	if err != nil {
		c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
		return
	}
	if req.Amount < getMinTopup() {
		c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())})
		return
	}

	id := c.GetInt("id")
	group, err := model.GetUserGroup(id, true)
	if err != nil {
		c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
		return
	}
	payMoney := getPayMoney(req.Amount, group)
	if payMoney < 0.01 {
		c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
		return
	}

	paymentMethod := req.PaymentMethod
	if !operation_setting.ContainsPayMethod(paymentMethod) {
		c.JSON(200, gin.H{"message": "error", "data": "支付方式不存在"})
		return
	}
	if operation_setting.OnlinePayProvider == "yipay" {
		payURL, params, yipayErr := requestYipayOrder(req, id, payMoney, paymentMethod)
		if yipayErr != nil {
			c.JSON(200, gin.H{"message": "error", "data": yipayErr.Error()})
			return
		}
		c.JSON(200, gin.H{"message": "success", "data": params, "url": payURL})
		return
	}

	callBackAddress := service.GetCallbackAddress()
	returnUrl, _ := url.Parse(system_setting.ServerAddress + "/console/log")
	notifyUrl, _ := url.Parse(buildUserEpayNotifyURL(callBackAddress))
	tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
	tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
	client := GetEpayClient()
	if client == nil {
		c.JSON(200, gin.H{"message": "error", "data": "当前管理员未配置支付信息"})
		return
	}
	uri, params, err := client.Purchase(&epay.PurchaseArgs{
		Type:           paymentMethod,
		ServiceTradeNo: tradeNo,
		Name:           fmt.Sprintf("TUC%d", req.Amount),
		Money:          strconv.FormatFloat(payMoney, 'f', 2, 64),
		Device:         epay.PC,
		NotifyUrl:      notifyUrl,
		ReturnUrl:      returnUrl,
	})
	if err != nil {
		c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
		return
	}
	amount := req.Amount
	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
		dAmount := decimal.NewFromInt(int64(amount))
		dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
		amount = dAmount.Div(dQuotaPerUnit).IntPart()
	}
	topUp := &model.TopUp{
		UserId:        id,
		Amount:        amount,
		Money:         payMoney,
		TradeNo:       tradeNo,
		PaymentMethod: paymentMethod,
		CreateTime:    time.Now().Unix(),
		Status:        "pending",
	}
	err = topUp.Insert()
	if err != nil {
		c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
		return
	}
	c.JSON(200, gin.H{"message": "success", "data": params, "url": uri})
}

// tradeNo lock
var orderLocks sync.Map
var createLock sync.Mutex

// refCountedMutex 带引用计数的互斥锁，确保最后一个使用者才从 map 中删除
type refCountedMutex struct {
	mu       sync.Mutex
	refCount int
}

// LockOrder 尝试对给定订单号加锁
func LockOrder(tradeNo string) {
	createLock.Lock()
	var rcm *refCountedMutex
	if v, ok := orderLocks.Load(tradeNo); ok {
		rcm = v.(*refCountedMutex)
	} else {
		rcm = &refCountedMutex{}
		orderLocks.Store(tradeNo, rcm)
	}
	rcm.refCount++
	createLock.Unlock()
	rcm.mu.Lock()
}

// UnlockOrder 释放给定订单号的锁
func UnlockOrder(tradeNo string) {
	v, ok := orderLocks.Load(tradeNo)
	if !ok {
		return
	}
	rcm := v.(*refCountedMutex)
	rcm.mu.Unlock()

	createLock.Lock()
	rcm.refCount--
	if rcm.refCount == 0 {
		orderLocks.Delete(tradeNo)
	}
	createLock.Unlock()
}

func EpayNotify(c *gin.Context) {
	var params map[string]string

	if c.Request.Method == "POST" {
		// POST 请求：从 POST body 解析参数
		if err := c.Request.ParseForm(); err != nil {
			log.Println("易支付回调POST解析失败:", err)
			_, _ = c.Writer.Write([]byte("fail"))
			return
		}
		params = lo.Reduce(lo.Keys(c.Request.PostForm), func(r map[string]string, t string, i int) map[string]string {
			r[t] = c.Request.PostForm.Get(t)
			return r
		}, map[string]string{})
		// 兼容 JSON 回调体（部分 Yipay/Jeepay 部署会使用 application/json 推送）。
		if len(params) == 0 {
			bodyBytes, readErr := io.ReadAll(c.Request.Body)
			if readErr == nil && len(bodyBytes) > 0 {
				var payload map[string]any
				if unmarshalErr := common.Unmarshal(bodyBytes, &payload); unmarshalErr == nil {
					params = lo.Reduce(lo.Keys(payload), func(r map[string]string, t string, i int) map[string]string {
						r[t] = fmt.Sprintf("%v", payload[t])
						return r
					}, map[string]string{})
				}
			}
		}
	} else {
		// GET 请求：从 URL Query 解析参数
		params = lo.Reduce(lo.Keys(c.Request.URL.Query()), func(r map[string]string, t string, i int) map[string]string {
			r[t] = c.Request.URL.Query().Get(t)
			return r
		}, map[string]string{})
	}

	if len(params) == 0 {
		log.Printf("易支付回调参数为空，method=%s, contentType=%s, remoteIP=%s, rawQuery=%s", c.Request.Method, c.ContentType(), c.ClientIP(), c.Request.URL.RawQuery)
		_, _ = c.Writer.Write([]byte("fail"))
		return
	}
	// 回调入站日志：用于确认回调是否到达以及携带了哪些参数。
	log.Printf("支付回调已到达，provider=%s, method=%s, contentType=%s, remoteIP=%s, keys=%v",
		operation_setting.OnlinePayProvider, c.Request.Method, c.ContentType(), c.ClientIP(), sortedParamKeys(params))

	// 先尝试按 Yipay 回调处理：使用 Yipay AppSecret 做 MD5 验签，并按 mchOrderNo/state 更新订单。
	if operation_setting.OnlinePayProvider == "yipay" {
		log.Printf("Yipay 回调关键参数：mchOrderNo=%s, state=%s, payOrderId=%s",
			params["mchOrderNo"], params["state"], params["payOrderId"])
		if !verifyYipayNotify(params) {
			log.Printf("Yipay 回调签名验证失败，mchOrderNo=%s, state=%s", params["mchOrderNo"], params["state"])
			_, _ = c.Writer.Write([]byte("fail"))
			return
		}
		tradeNo := strings.TrimSpace(params["mchOrderNo"])
		state := strings.TrimSpace(params["state"])
		if tradeNo == "" {
			log.Println("Yipay 回调缺少 mchOrderNo")
			_, _ = c.Writer.Write([]byte("fail"))
			return
		}
		if state != "2" {
			log.Printf("Yipay 回调非成功状态，mchOrderNo=%s, state=%s", tradeNo, state)
			_, _ = c.Writer.Write([]byte("success"))
			return
		}
		LockOrder(tradeNo)
		defer UnlockOrder(tradeNo)
		topUp := model.GetTopUpByTradeNo(tradeNo)
		if topUp == nil {
			log.Printf("Yipay 回调未找到订单: %s", tradeNo)
			_, _ = c.Writer.Write([]byte("fail"))
			return
		}
		if topUp.Status == "pending" {
			topUp.Status = "success"
			if err := topUp.Update(); err != nil {
				log.Printf("Yipay 回调更新订单失败: %v", topUp)
				_, _ = c.Writer.Write([]byte("fail"))
				return
			}
			dAmount := decimal.NewFromInt(int64(topUp.Amount))
			dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
			quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart())
			if err := model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true); err != nil {
				log.Printf("Yipay 回调更新用户失败: %v", topUp)
				_, _ = c.Writer.Write([]byte("fail"))
				return
			}
			log.Printf("Yipay 回调更新用户成功 %v", topUp)
			model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功，充值金额: %v，支付金额：%f", logger.LogQuota(quotaToAdd), topUp.Money))
			// 与易支付成功路径一致：到账后按邀请关系给邀请人分销提成
			model.ApplyAffiliateTopupReward(topUp.UserId, quotaToAdd)
		}
		_, _ = c.Writer.Write([]byte("success"))
		return
	}

	client := GetEpayClient()
	if client == nil {
		log.Println("易支付回调失败 未找到配置信息")
		_, err := c.Writer.Write([]byte("fail"))
		if err != nil {
			log.Println("易支付回调写入失败")
		}
		return
	}
	verifyInfo, err := client.Verify(params)
	if err == nil && verifyInfo.VerifyStatus {
		_, err := c.Writer.Write([]byte("success"))
		if err != nil {
			log.Println("易支付回调写入失败")
		}
	} else {
		_, err := c.Writer.Write([]byte("fail"))
		if err != nil {
			log.Println("易支付回调写入失败")
		}
		log.Println("易支付回调签名验证失败")
		return
	}

	if verifyInfo.TradeStatus == epay.StatusTradeSuccess {
		log.Println(verifyInfo)
		LockOrder(verifyInfo.ServiceTradeNo)
		defer UnlockOrder(verifyInfo.ServiceTradeNo)
		topUp := model.GetTopUpByTradeNo(verifyInfo.ServiceTradeNo)
		if topUp == nil {
			log.Printf("易支付回调未找到订单: %v", verifyInfo)
			return
		}
		if topUp.Status == "pending" {
			topUp.Status = "success"
			err := topUp.Update()
			if err != nil {
				log.Printf("易支付回调更新订单失败: %v", topUp)
				return
			}
			//user, _ := model.GetUserById(topUp.UserId, false)
			//user.Quota += topUp.Amount * 500000
			dAmount := decimal.NewFromInt(int64(topUp.Amount))
			dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
			quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart())
			err = model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true)
			if err != nil {
				log.Printf("易支付回调更新用户失败: %v", topUp)
				return
			}
			log.Printf("易支付回调更新用户成功 %v", topUp)
			model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功，充值金额: %v，支付金额：%f", logger.LogQuota(quotaToAdd), topUp.Money))
			// 支付回调成功且额度已入账后发放邀请人分销奖励（与 model.Recharge / Yipay 等路径一致）
			model.ApplyAffiliateTopupReward(topUp.UserId, quotaToAdd)
		}
	} else {
		log.Printf("易支付异常回调: %v", verifyInfo)
	}
}

func RequestAmount(c *gin.Context) {
	var req AmountRequest
	err := c.ShouldBindJSON(&req)
	if err != nil {
		c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
		return
	}

	if req.Amount < getMinTopup() {
		c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())})
		return
	}
	id := c.GetInt("id")
	group, err := model.GetUserGroup(id, true)
	if err != nil {
		c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
		return
	}
	payMoney := getPayMoney(req.Amount, group)
	if payMoney <= 0.01 {
		c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
		return
	}
	c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
}

// parseTopUpListStatusFilter 解析充值列表的 status 查询参数，仅允许预定义状态；非法或 all 视为不按状态筛选。
func parseTopUpListStatusFilter(c *gin.Context) string {
	s := strings.TrimSpace(strings.ToLower(c.Query("status")))
	if s == "" || s == "all" {
		return ""
	}
	switch s {
	case common.TopUpStatusPending, common.TopUpStatusSuccess, common.TopUpStatusFailed, common.TopUpStatusExpired:
		return s
	default:
		return ""
	}
}

// parseTopUpTradeNoKeyword 解析订单号筛选：优先 trade_no，其次兼容旧参数 keyword。
func parseTopUpTradeNoKeyword(c *gin.Context) string {
	if v := strings.TrimSpace(c.Query("trade_no")); v != "" {
		return v
	}
	return strings.TrimSpace(c.Query("keyword"))
}

func GetUserTopUps(c *gin.Context) {
	userId := c.GetInt("id")
	pageInfo := common.GetPageQuery(c)
	tradeNoKeyword := parseTopUpTradeNoKeyword(c)
	statusFilter := parseTopUpListStatusFilter(c)

	var (
		topups []*model.TopUp
		total  int64
		err    error
	)
	topups, total, err = model.GetUserTopUps(userId, pageInfo, statusFilter, tradeNoKeyword)
	if err != nil {
		common.ApiError(c, err)
		return
	}

	pageInfo.SetTotal(int(total))
	pageInfo.SetItems(topups)
	common.ApiSuccess(c, pageInfo)
}

// GetAllTopUps 管理员获取全平台充值记录
func GetAllTopUps(c *gin.Context) {
	pageInfo := common.GetPageQuery(c)
	tradeNoKeyword := parseTopUpTradeNoKeyword(c)
	usernameKeyword := strings.TrimSpace(c.Query("username"))
	statusFilter := parseTopUpListStatusFilter(c)

	var (
		topups []*model.TopUp
		total  int64
		err    error
	)
	topups, total, err = model.GetAllTopUps(pageInfo, statusFilter, tradeNoKeyword, usernameKeyword)
	if err != nil {
		common.ApiError(c, err)
		return
	}

	pageInfo.SetTotal(int(total))
	pageInfo.SetItems(topups)
	common.ApiSuccess(c, pageInfo)
}

type AdminCompleteTopupRequest struct {
	TradeNo string `json:"trade_no"`
}

// AdminCompleteTopUp 管理员补单接口
func AdminCompleteTopUp(c *gin.Context) {
	var req AdminCompleteTopupRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.TradeNo == "" {
		common.ApiErrorMsg(c, "参数错误")
		return
	}

	// 订单级互斥，防止并发补单
	LockOrder(req.TradeNo)
	defer UnlockOrder(req.TradeNo)

	// adminUsername 用于补单后写入使用日志详情，便于追溯具体操作者。
	adminUsername := c.GetString("username")
	if err := model.ManualCompleteTopUp(req.TradeNo, adminUsername); err != nil {
		common.ApiError(c, err)
		return
	}
	common.ApiSuccess(c, nil)
}
