package pay

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"bindbox-game/configs"
	"bindbox-game/internal/code"
	"bindbox-game/internal/pkg/core"
	lotterynotify "bindbox-game/internal/pkg/notify"
	pay "bindbox-game/internal/pkg/pay"
	"bindbox-game/internal/pkg/wechat"
	"bindbox-game/internal/repository/mysql/model"
	strat "bindbox-game/internal/service/activity/strategy"
	usersvc "bindbox-game/internal/service/user"

	"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
	"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
	"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
	"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
	"github.com/wechatpay-apiv3/wechatpay-go/utils"
)

type notifyAck struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

// WechatNotify 微信支付回调通知处理
// 入参：微信官方通知，验签并解密resource，推进订单状态为已支付
// 幂等：若订单已为已支付则直接ACK
// @Summary 微信支付回调通知
// @Description 接收微信支付结果通知，验证签名并处理订单状态
// @Tags Pay
// @Accept json
// @Produce json
// @Success 200 {object} notifyAck "处理成功"
// @Failure 400 {object} notifyAck "请求参数错误"
// @Failure 500 {object} notifyAck "服务器内部错误"
// @Router /pay/wechat/notify [post]
func (h *handler) WechatNotify() core.HandlerFunc {
	return func(ctx core.Context) {
		c := configs.Get()
		if c.WechatPay.ApiV3Key == "" {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete"))
			return
		}
		var handler *notify.Handler
		if c.WechatPay.PublicKeyID != "" && c.WechatPay.PublicKeyPath != "" {
			pubKey, err := utils.LoadPublicKeyWithPath(c.WechatPay.PublicKeyPath)
			if err != nil {
				ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150001, err.Error()))
				return
			}
			handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAPubkeyVerifier(c.WechatPay.PublicKeyID, *pubKey))
		} else {
			if c.WechatPay.MchID == "" || c.WechatPay.SerialNo == "" || c.WechatPay.PrivateKeyPath == "" {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete"))
				return
			}
			mchPrivateKey, err := utils.LoadPrivateKeyWithPath(c.WechatPay.PrivateKeyPath)
			if err != nil {
				ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150002, err.Error()))
				return
			}
			if err := downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx.RequestContext(), mchPrivateKey, c.WechatPay.SerialNo, c.WechatPay.MchID, c.WechatPay.ApiV3Key); err != nil {
				ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150003, err.Error()))
				return
			}
			certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(c.WechatPay.MchID)
			handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor))
		}

		var transaction payments.Transaction
		notification, err := handler.ParseNotifyRequest(ctx.RequestContext(), ctx.Request(), &transaction)
		if err != nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error()))
			return
		}
		// 事件去重处理
		var existed *model.PaymentNotifyEvents
		if notification != nil && notification.ID != "" {
			existed, _ = h.readDB.PaymentNotifyEvents.WithContext(ctx.RequestContext()).Where(h.readDB.PaymentNotifyEvents.NotifyID.Eq(notification.ID)).First()
			if existed != nil && existed.Processed {
				ctx.Payload(&notifyAck{Code: "SUCCESS", Message: "OK"})
				return
			}
		}
		if transaction.OutTradeNo == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "missing out_trade_no"))
			return
		}
		paidAt := time.Now()
		if transaction.SuccessTime != nil {
			if t, err := time.Parse(time.RFC3339, *transaction.SuccessTime); err == nil {
				paidAt = t
			}
		}
		rawStr := func() string { b, _ := json.Marshal(transaction); return string(b) }()
		order, err := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(*transaction.OutTradeNo)).First()
		if err != nil || order == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "order not found for out_trade_no"))
			return
		}
		tx := &model.PaymentTransactions{
			OrderID: order.ID,
			OrderNo: *transaction.OutTradeNo,
			Channel: "wechat_jsapi",
			TransactionID: func() string {
				if transaction.TransactionId != nil {
					return *transaction.TransactionId
				}
				return ""
			}(),
			AmountTotal: func() int64 {
				if transaction.Amount != nil && transaction.Amount.Total != nil {
					return *transaction.Amount.Total
				}
				return 0
			}(),
			SuccessTime: paidAt,
			Raw:         rawStr,
		}
		if err := h.writeDB.PaymentTransactions.WithContext(ctx.RequestContext()).Create(tx); err != nil {
			ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150006, err.Error()))
			return
		}
		// 记录事件
		if notification != nil && notification.ID != "" {
			if existed == nil {
				_ = h.writeDB.PaymentNotifyEvents.WithContext(ctx.RequestContext()).Create(&model.PaymentNotifyEvents{
					NotifyID:     notification.ID,
					ResourceType: notification.ResourceType,
					EventType:    notification.EventType,
					Summary:      notification.Summary,
					Raw:          rawStr,
					Processed:    false,
				})
			}
		}
		_, err = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.OrderNo.Eq(*transaction.OutTradeNo), h.writeDB.Orders.Status.Eq(1)).Updates(map[string]any{
			h.writeDB.Orders.Status.ColumnName().String(): 2,
			h.writeDB.Orders.PaidAt.ColumnName().String(): paidAt,
		})
		if err != nil {
			ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150005, err.Error()))
			return
		}
		ord, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(*transaction.OutTradeNo)).First()
		// 支付成功后扣减优惠券余额（优先使用结构化明细表），如无明细再降级解析备注
		if ord != nil {
			var ocnt int64
			_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM order_coupons WHERE order_id=?", ord.ID).Scan(&ocnt).Error
			if ocnt > 0 {
				_ = h.user.DeductCouponsForPaidOrder(ctx.RequestContext(), ord.UserID, ord.ID, paidAt)
			}
			// 从备注解析所有优惠券使用片段 |c:<id>:<applied>
			remark := ord.Remark
			parts := strings.Split(remark, "|")
			for _, seg := range parts {
				if ocnt > 0 {
					break
				} // 已使用结构化明细，跳过备注解析
				if strings.HasPrefix(seg, "c:") {
					// seg: c:<id>:<amount>
					xs := strings.Split(seg, ":")
					if len(xs) == 3 {
						// 解析ID与金额
						// 并根据券类型处理余额扣减或一次性核销
						// xs[1] user_coupon_id, xs[2] applied_amount
						// 查找用户券与模板
						var uc *model.UserCoupons
						uc, _ = h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(parseInt64(xs[1])), h.readDB.UserCoupons.UserID.Eq(ord.UserID)).First()
						if uc != nil {
							// 幂等保护：已核销且已绑定本订单则跳过
							if uc.Status == 2 && uc.UsedOrderID == ord.ID {
								continue
							}
							sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID), h.readDB.SystemCoupons.Status.Eq(1)).First()
							applied := parseInt64(xs[2])
							if sc != nil {
								if sc.DiscountType == 1 { // 金额券，扣减余额
									// 读取余额
									var bal int64
									_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", uc.ID).Scan(&bal).Error
									newBal := bal - applied
									if newBal < 0 {
										newBal = 0
									}
									if newBal == 0 {
										_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(uc.ID)).Updates(map[string]any{
											"balance_amount": newBal,
											h.readDB.UserCoupons.Status.ColumnName().String():      2,
											h.readDB.UserCoupons.UsedOrderID.ColumnName().String(): ord.ID,
											h.readDB.UserCoupons.UsedAt.ColumnName().String():      paidAt,
										})
									} else {
										_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(uc.ID)).Updates(map[string]any{
											"balance_amount": newBal,
											h.readDB.UserCoupons.UsedOrderID.ColumnName().String(): ord.ID,
											h.readDB.UserCoupons.UsedAt.ColumnName().String():      paidAt,
										})
									}
								} else { // 满减/折扣券，一次性核销
									_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(uc.ID)).Updates(map[string]any{
										h.readDB.UserCoupons.Status.ColumnName().String():      2,
										h.readDB.UserCoupons.UsedOrderID.ColumnName().String(): ord.ID,
										h.readDB.UserCoupons.UsedAt.ColumnName().String():      paidAt,
									})
								}
							}
						}
					}
				}
			}
		}
		if ord != nil {
			func() {
				cfg, _ := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq("points_reward_per_cent")).First()
				rate := int64(0)
				if cfg != nil {
					var r int64
					_, _ = fmt.Sscanf(cfg.ConfigValue, "%d", &r)
					if r > 0 {
						rate = r
					}
				}
				if rate > 0 && ord.ActualAmount > 0 {
					reward := ord.ActualAmount * rate
					_ = h.user.AddPointsWithAction(ctx.RequestContext(), ord.UserID, reward, "pay_reward", ord.OrderNo, "pay_reward", nil, nil)
				}
			}()
		}
		if ord != nil && ord.SourceType == 2 {
			iss := parseIssueIDFromRemark(ord.Remark)
			aid := parseActivityIDFromRemark(ord.Remark)
			dc := parseCountFromRemark(ord.Remark)
			if dc <= 0 {
				dc = 1
			}
			act, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(aid)).First()

			// 【修复】一番赏玩法：无论 instant 还是 scheduled 模式，支付成功后都要先占用位置
			if act != nil && act.PlayType == "ichiban" {
				idxs, cnts := parseSlotsCountsFromRemark(ord.Remark)
				fmt.Printf("[支付回调-一番赏占位] 解析格位 idxs=%v cnts=%v 订单备注=%s 模式=%s\n", idxs, cnts, ord.Remark, act.DrawMode)
				rem := make([]int64, len(cnts))
				copy(rem, cnts)
				cur := 0
				for i := int64(0); i < dc; i++ {
					slot := func() int64 {
						if len(idxs) > 0 && len(idxs) == len(rem) {
							for cur < len(rem) && rem[cur] == 0 {
								cur++
							}
							if cur >= len(rem) {
								return -1
							}
							rem[cur]--
							return idxs[cur] - 1
						}
						return parseSlotFromRemark(ord.Remark)
					}()
					fmt.Printf("[支付回调-一番赏占位] 获取格位 slot=%d\n", slot)
					if slot < 0 {
						fmt.Printf("[支付回调-一番赏占位] ❌ 格位无效，跳出循环\n")
						break
					}
					var cnt int64
					cnt, _ = h.readDB.IssuePositionClaims.WithContext(ctx.RequestContext()).Where(h.readDB.IssuePositionClaims.IssueID.Eq(iss), h.readDB.IssuePositionClaims.SlotIndex.Eq(slot)).Count()
					fmt.Printf("[支付回调-一番赏占位] 检查格位占用 issueID=%d slot=%d 已占用数=%d\n", iss, slot, cnt)
					if cnt > 0 {
						fmt.Printf("[支付回调-一番赏占位] ❌ 格位已被占用，跳出循环并退款\n")
						// 标记订单为退款状态并处理退款
						wc, e := pay.NewWechatPayClient(ctx.RequestContext())
						if e == nil {
							refundNo := fmt.Sprintf("R%s-%d", ord.OrderNo, time.Now().Unix())
							refundID, status, e2 := wc.RefundOrder(ctx.RequestContext(), ord.OrderNo, refundNo, ord.ActualAmount, ord.ActualAmount, "slot_unavailable")
							if e2 == nil {
								_ = h.writeDB.PaymentRefunds.WithContext(ctx.RequestContext()).Create(&model.PaymentRefunds{OrderID: ord.ID, OrderNo: ord.OrderNo, RefundNo: refundNo, Channel: "wechat_jsapi", Status: status, AmountRefund: ord.ActualAmount, Reason: "slot_unavailable"})
								_ = h.writeDB.UserPointsLedger.WithContext(ctx.RequestContext()).Create(&model.UserPointsLedger{UserID: ord.UserID, Action: "refund_amount", Points: ord.ActualAmount / 100, RefTable: "payment_refund", RefID: refundID})
								_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.ID.Eq(ord.ID)).Updates(map[string]any{h.writeDB.Orders.Status.ColumnName().String(): 4})
								_ = h.repo.GetDbW().Exec("INSERT INTO lottery_refund_logs(issue_id, order_id, user_id, amount, coupon_type, coupon_amount, reason, status) VALUES(?,?,?,?,?,?,?,?)", iss, ord.ID, ord.UserID, ord.ActualAmount, "", 0, "slot_unavailable", status).Error
							}
						}
						break
					}
					// 尝试创建占用记录，利用唯一索引防止并发
					err := h.writeDB.IssuePositionClaims.WithContext(ctx.RequestContext()).Create(&model.IssuePositionClaims{IssueID: iss, SlotIndex: slot, UserID: ord.UserID, OrderID: ord.ID})
					if err != nil {
						fmt.Printf("[支付回调-一番赏占位] ❌ 创建格位占用失败 err=%v\n", err)
						// 同样处理退款
						wc, e := pay.NewWechatPayClient(ctx.RequestContext())
						if e == nil {
							refundNo := fmt.Sprintf("R%s-%d", ord.OrderNo, time.Now().Unix())
							refundID, status, e2 := wc.RefundOrder(ctx.RequestContext(), ord.OrderNo, refundNo, ord.ActualAmount, ord.ActualAmount, "slot_concurrent_conflict")
							if e2 == nil {
								_ = h.writeDB.PaymentRefunds.WithContext(ctx.RequestContext()).Create(&model.PaymentRefunds{OrderID: ord.ID, OrderNo: ord.OrderNo, RefundNo: refundNo, Channel: "wechat_jsapi", Status: status, AmountRefund: ord.ActualAmount, Reason: "slot_concurrent_conflict"})
								_ = h.writeDB.UserPointsLedger.WithContext(ctx.RequestContext()).Create(&model.UserPointsLedger{UserID: ord.UserID, Action: "refund_amount", Points: ord.ActualAmount / 100, RefTable: "payment_refund", RefID: refundID})
								_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.ID.Eq(ord.ID)).Updates(map[string]any{h.writeDB.Orders.Status.ColumnName().String(): 4})
								_ = h.repo.GetDbW().Exec("INSERT INTO lottery_refund_logs(issue_id, order_id, user_id, amount, coupon_type, coupon_amount, reason, status) VALUES(?,?,?,?,?,?,?,?)", iss, ord.ID, ord.UserID, ord.ActualAmount, "", 0, "slot_concurrent_conflict", status).Error
							}
						}
						break
					}
					fmt.Printf("[支付回调-一番赏占位] ✅ 格位占用成功 issueID=%d slot=%d userID=%d orderID=%d\n", iss, slot, ord.UserID, ord.ID)
				}
			}

			// instant 模式才立即开奖发奖
			if act != nil && act.DrawMode == "instant" {
				logs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(ord.ID)).Find()
				done := int64(len(logs))
				// 解析道具卡ID
				icID := parseItemCardIDFromRemark(ord.Remark)
				fmt.Printf("[支付回调-抽奖] 开始处理 活动ID=%d 期ID=%d 次数=%d 道具卡ID=%d 玩法=%s 已完成=%d\n", aid, iss, dc, icID, act.PlayType, done)
				if act.PlayType == "ichiban" {
					idxs, cnts := parseSlotsCountsFromRemark(ord.Remark)
					fmt.Printf("[支付回调-抽奖] 解析格位 idxs=%v cnts=%v 订单备注=%s\n", idxs, cnts, ord.Remark)
					rem := make([]int64, len(cnts))
					copy(rem, cnts)
					cur := 0
					for i := done; i < dc; i++ {
						fmt.Printf("[支付回调-抽奖] 循环开始 i=%d dc=%d\n", i, dc)
						slot := func() int64 {
							if len(idxs) > 0 && len(idxs) == len(rem) {
								for cur < len(rem) && rem[cur] == 0 {
									cur++
								}
								if cur >= len(rem) {
									return -1
								}
								rem[cur]--
								return idxs[cur] - 1
							}
							return parseSlotFromRemark(ord.Remark)
						}()
						fmt.Printf("[支付回调-抽奖] 获取格位 slot=%d\n", slot)
						if slot < 0 {
							fmt.Printf("[支付回调-抽奖] ❌ 格位无效，跳出循环\n")
							break
						}
						// 位置已在上面占用，这里直接选择奖品
						rid, _, e2 := strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), aid, iss, slot)
						fmt.Printf("[支付回调-抽奖] SelectItemBySlot 结果 rid=%d err=%v\n", rid, e2)
						if e2 != nil || rid <= 0 {
							fmt.Printf("[支付回调-抽奖] ❌ SelectItemBySlot 失败，跳出循环\n")
							break
						}
						rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
						if rw == nil {
							fmt.Printf("[支付回调-抽奖] ❌ 奖品设置为空，跳出循环\n")
							break
						}
						fmt.Printf("[支付回调-抽奖] 发放奖品 rid=%d 奖品名=%s\n", rid, rw.Name)
						// 【先记录日志，再发奖】确保日志创建成功后再发奖，防止重复
						log := &model.ActivityDrawLogs{UserID: ord.UserID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
						if errLog := h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log); errLog != nil {
							fmt.Printf("[支付回调-抽奖] ❌ 创建开奖日志失败 err=%v，可能已存在，跳过\n", errLog)
							continue
						}
						_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), ord.UserID, rid)
						// 道具卡效果处理
						fmt.Printf("[支付回调-道具卡] 开始检查 活动允许道具卡=%t 道具卡ID=%d\n", act.AllowItemCards, icID)
						if act.AllowItemCards && icID > 0 {
							uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(icID), h.readDB.UserItemCards.UserID.Eq(ord.UserID), h.readDB.UserItemCards.Status.Eq(1)).First()
							if uic != nil {
								fmt.Printf("[支付回调-道具卡] 找到用户道具卡 ID=%d CardID=%d Status=%d\n", uic.ID, uic.CardID, uic.Status)
								ic, _ := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.SystemItemCards.ID.Eq(uic.CardID), h.readDB.SystemItemCards.Status.Eq(1)).First()
								now := time.Now()
								if ic != nil && !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
									scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == aid) || (ic.ScopeType == 4 && ic.IssueID == iss)
									fmt.Printf("[支付回调-道具卡] 系统道具卡 EffectType=%d RewardMultiplierX1000=%d ScopeType=%d scopeOK=%t\n", ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, scopeOK)
									if scopeOK {
										if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
											fmt.Printf("[支付回调-道具卡] ✅ 应用双倍奖励 奖品ID=%d 奖品名=%s\n", rid, rw.Name)
											_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), ord.UserID, rid)
										}
										// 核销道具卡
										fmt.Printf("[支付回调-道具卡] 核销道具卡 用户道具卡ID=%d\n", icID)
										_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(icID), h.readDB.UserItemCards.UserID.Eq(ord.UserID), h.readDB.UserItemCards.Status.Eq(1)).Updates(map[string]any{
											h.readDB.UserItemCards.Status.ColumnName().String():         2,
											h.readDB.UserItemCards.UsedDrawLogID.ColumnName().String():  log.ID,
											h.readDB.UserItemCards.UsedActivityID.ColumnName().String(): aid,
											h.readDB.UserItemCards.UsedIssueID.ColumnName().String():    iss,
											h.readDB.UserItemCards.UsedAt.ColumnName().String():         now,
										})
									}
								}
							} else {
								fmt.Printf("[支付回调-道具卡] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", ord.UserID, icID)
							}
						}
					}
				} else {
					sel := strat.NewDefault(h.readDB, h.writeDB)
					for i := done; i < dc; i++ {
						rid, _, e2 := sel.SelectItem(ctx.RequestContext(), aid, iss, ord.UserID)
						if e2 != nil || rid <= 0 {
							break
						}
						rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
						if rw == nil {
							break
						}
						// 【先记录日志，再发奖】确保日志创建成功后再发奖，防止重复
						log := &model.ActivityDrawLogs{UserID: ord.UserID, IssueID: iss, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
						if errLog := h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log); errLog != nil {
							fmt.Printf("[支付回调-默认玩法] ❌ 创建开奖日志失败 err=%v，可能已存在，跳过\n", errLog)
							break
						}
						_, errGrant := h.user.GrantRewardToOrder(ctx.RequestContext(), ord.UserID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &aid, RewardID: &rid, Remark: rw.Name})
						if errGrant != nil {
							fmt.Printf("[支付回调-默认玩法] ❌ 发奖失败 err=%v，执行退款\n", errGrant)
							// 发奖失败，执行退款
							wc, e := pay.NewWechatPayClient(ctx.RequestContext())
							if e == nil {
								refundNo := fmt.Sprintf("R%s-%d", ord.OrderNo, time.Now().Unix())
								refundID, status, e2 := wc.RefundOrder(ctx.RequestContext(), ord.OrderNo, refundNo, ord.ActualAmount, ord.ActualAmount, "grant_reward_failed")
								if e2 == nil {
									_ = h.writeDB.PaymentRefunds.WithContext(ctx.RequestContext()).Create(&model.PaymentRefunds{OrderID: ord.ID, OrderNo: ord.OrderNo, RefundNo: refundNo, Channel: "wechat_jsapi", Status: status, AmountRefund: ord.ActualAmount, Reason: "grant_reward_failed"})
									_ = h.writeDB.UserPointsLedger.WithContext(ctx.RequestContext()).Create(&model.UserPointsLedger{UserID: ord.UserID, Action: "refund_amount", Points: ord.ActualAmount / 100, RefTable: "payment_refund", RefID: refundID})
									_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.ID.Eq(ord.ID)).Updates(map[string]any{h.writeDB.Orders.Status.ColumnName().String(): 4})
									// 记录退款日志
									_ = h.repo.GetDbW().Exec("INSERT INTO lottery_refund_logs(issue_id, order_id, user_id, amount, coupon_type, coupon_amount, reason, status) VALUES(?,?,?,?,?,?,?,?)", iss, ord.ID, ord.UserID, ord.ActualAmount, "", 0, "grant_reward_failed", status).Error
									// 标记开奖日志为无效或失败（可选，视业务需求而定，这里暂不删除日志以便追溯）
								}
							}
							break
						}
						// 道具卡效果处理
						icID := parseItemCardIDFromRemark(ord.Remark)
						fmt.Printf("[支付回调-道具卡] 开始检查 活动允许道具卡=%t 道具卡ID=%d\n", act.AllowItemCards, icID)
						if act.AllowItemCards && icID > 0 {
							uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(icID), h.readDB.UserItemCards.UserID.Eq(ord.UserID), h.readDB.UserItemCards.Status.Eq(1)).First()
							if uic != nil {
								fmt.Printf("[支付回调-道具卡] 找到用户道具卡 ID=%d CardID=%d Status=%d\n", uic.ID, uic.CardID, uic.Status)
								ic, _ := h.readDB.SystemItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.SystemItemCards.ID.Eq(uic.CardID), h.readDB.SystemItemCards.Status.Eq(1)).First()
								now := time.Now()
								if ic != nil && !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
									scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == aid) || (ic.ScopeType == 4 && ic.IssueID == iss)
									fmt.Printf("[支付回调-道具卡] 系统道具卡 EffectType=%d RewardMultiplierX1000=%d ScopeType=%d scopeOK=%t\n", ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, scopeOK)
									if scopeOK {
										if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
											fmt.Printf("[支付回调-道具卡] ✅ 应用双倍奖励 奖品ID=%d 奖品名=%s\n", rid, rw.Name)
											_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), ord.UserID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &aid, RewardID: &rid, Remark: rw.Name + "(倍数)"})
										}
										// 核销道具卡
										fmt.Printf("[支付回调-道具卡] 核销道具卡 用户道具卡ID=%d\n", icID)
										_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(icID), h.readDB.UserItemCards.UserID.Eq(ord.UserID), h.readDB.UserItemCards.Status.Eq(1)).Updates(map[string]any{
											h.readDB.UserItemCards.Status.ColumnName().String():         2,
											h.readDB.UserItemCards.UsedDrawLogID.ColumnName().String():  log.ID,
											h.readDB.UserItemCards.UsedActivityID.ColumnName().String(): aid,
											h.readDB.UserItemCards.UsedIssueID.ColumnName().String():    iss,
											h.readDB.UserItemCards.UsedAt.ColumnName().String():         now,
										})
									}
								}
							} else {
								fmt.Printf("[支付回调-道具卡] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", ord.UserID, icID)
							}
						}
					}
				}
				// 【开奖后虚拟发货】即时开奖完成后上传虚拟发货
				go func(orderID int64, orderNo string, userID int64, actName string) {
					drawLogs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(orderID)).Find()
					if len(drawLogs) == 0 {
						fmt.Printf("[即时开奖-虚拟发货] 没有开奖记录，跳过 order_id=%d\n", orderID)
						return
					}
					// 收集赏品名称
					var rewardNames []string
					for _, lg := range drawLogs {
						if rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(lg.RewardID)).First(); rw != nil {
							rewardNames = append(rewardNames, rw.Name)
						}
					}
					itemsDesc := actName + " " + orderNo + " 盲盒赏品: " + strings.Join(rewardNames, ", ")
					if len(itemsDesc) > 120 {
						itemsDesc = itemsDesc[:120]
					}
					// 获取支付交易信息
					var tx *model.PaymentTransactions
					tx, _ = h.readDB.PaymentTransactions.WithContext(ctx.RequestContext()).Where(h.readDB.PaymentTransactions.OrderNo.Eq(orderNo)).First()
					if tx == nil || tx.TransactionID == "" {
						fmt.Printf("[即时开奖-虚拟发货] 没有支付交易记录，跳过 order_no=%s\n", orderNo)
						return
					}
					// 获取用户openid
					var u *model.Users
					u, _ = h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(userID)).First()
					payerOpenid := ""
					if u != nil {
						payerOpenid = u.Openid
					}
					fmt.Printf("[即时开奖-虚拟发货] 上传 order_no=%s transaction_id=%s items_desc=%s\n", orderNo, tx.TransactionID, itemsDesc)
					if err := wechat.UploadVirtualShippingWithFallback(ctx, &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}, tx.TransactionID, orderNo, payerOpenid, itemsDesc, time.Now()); err != nil {
						fmt.Printf("[即时开奖-虚拟发货] 上传失败: %v\n", err)
					}
					// 【开奖后推送通知】
					notifyCfg := &lotterynotify.WechatNotifyConfig{
						AppID:                   c.Wechat.AppID,
						AppSecret:               c.Wechat.AppSecret,
						LotteryResultTemplateID: c.Wechat.LotteryResultTemplateID,
					}
					_ = lotterynotify.SendLotteryResultNotification(ctx.RequestContext(), notifyCfg, payerOpenid, actName, rewardNames, orderNo, time.Now())
				}(ord.ID, ord.OrderNo, ord.UserID, act.Name)
			}
		}
		if ord != nil {
			var itemsDesc string
			if xs, _ := h.readDB.OrderItems.WithContext(ctx.RequestContext()).Where(h.readDB.OrderItems.OrderID.Eq(ord.ID)).Find(); len(xs) > 0 {
				var parts []string
				for _, it := range xs {
					parts = append(parts, it.Title+"*"+func(q int64) string { return fmt.Sprintf("%d", q) }(it.Quantity))
				}
				s := strings.Join(parts, ", ")
				if len(s) > 120 {
					s = s[:120]
				}
				itemsDesc = s
			} else {
				itemsDesc = "订单" + ord.OrderNo
			}
			payerOpenid := ""
			if transaction.Payer != nil && transaction.Payer.Openid != nil {
				payerOpenid = *transaction.Payer.Openid
			}
			// 抽奖订单在开奖后发货，非抽奖订单在支付后立即发货
			if ord.SourceType != 2 {
				if transaction.TransactionId != nil && *transaction.TransactionId != "" {
					fmt.Printf("[支付回调] 虚拟发货 尝试上传 order_id=%d order_no=%s user_id=%d transaction_id=%s items_desc=%s payer_openid=%s\n", ord.ID, ord.OrderNo, ord.UserID, *transaction.TransactionId, itemsDesc, payerOpenid)
					if err := wechat.UploadVirtualShippingWithFallback(ctx, &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}, *transaction.TransactionId, ord.OrderNo, payerOpenid, itemsDesc, time.Now()); err != nil {
						fmt.Printf("[支付回调] 虚拟发货上传失败: %v\n", err)
					}
				}
			} else {
				fmt.Printf("[支付回调] 抽奖订单跳过虚拟发货，将在开奖后发货 order_id=%d order_no=%s\n", ord.ID, ord.OrderNo)
			}
		}
		// 标记事件处理完成
		if notification != nil && notification.ID != "" {
			_, _ = h.writeDB.PaymentNotifyEvents.WithContext(ctx.RequestContext()).Where(h.readDB.PaymentNotifyEvents.NotifyID.Eq(notification.ID)).Updates(map[string]any{
				h.writeDB.PaymentNotifyEvents.Processed.ColumnName().String(): true,
			})
		}
		ctx.Payload(&notifyAck{Code: "SUCCESS", Message: "OK"})
	}
}

func parseIssueIDFromRemark(remark string) int64 {
	if remark == "" {
		return 0
	}
	parts := strings.Split(remark, "|")
	for _, p := range parts {
		if strings.HasPrefix(p, "issue:") {
			return parseInt64(p[6:])
		}
	}
	return 0
}

func parseActivityIDFromRemark(remark string) int64 {
	if remark == "" {
		return 0
	}
	parts := strings.Split(remark, "|")
	for _, p := range parts {
		if strings.HasPrefix(p, "lottery:activity:") {
			return parseInt64(p[len("lottery:activity:"):])
		}
	}
	return 0
}

func parseCountFromRemark(remark string) int64 {
	if remark == "" {
		return 1
	}
	parts := strings.Split(remark, "|")
	for _, p := range parts {
		if strings.HasPrefix(p, "count:") {
			n := parseInt64(p[6:])
			if n <= 0 {
				return 1
			}
			return n
		}
	}
	return 1
}

func parseInt64(s string) int64 {
	var n int64
	for i := 0; i < len(s); i++ {
		c := s[i]
		if c < '0' || c > '9' {
			break
		}
		n = n*10 + int64(c-'0')
	}
	return n
}

func parseSlotsCountsFromRemark(remark string) ([]int64, []int64) {
	if remark == "" {
		return nil, nil
	}
	parts := strings.Split(remark, "|")
	for _, p := range parts {
		if strings.HasPrefix(p, "slots:") {
			pairs := p[6:]
			var idxs []int64
			var cnts []int64
			start := 0
			for start <= len(pairs) {
				end := start
				for end < len(pairs) && pairs[end] != ',' {
					end++
				}
				if end > start {
					a := pairs[start:end]
					var x, y int64
					j := 0
					for j < len(a) && a[j] >= '0' && a[j] <= '9' {
						x = x*10 + int64(a[j]-'0')
						j++
					}
					if j < len(a) && a[j] == ':' {
						j++
						for j < len(a) && a[j] >= '0' && a[j] <= '9' {
							y = y*10 + int64(a[j]-'0')
							j++
						}
					}
					if y > 0 {
						idxs = append(idxs, x+1)
						cnts = append(cnts, y)
					}
				}
				start = end + 1
			}
			return idxs, cnts
		}
	}
	return nil, nil
}

func parseSlotFromRemark(remark string) int64 {
	if remark == "" {
		return -1
	}
	p := 0
	for i := 0; i < len(remark); i++ {
		if remark[i] == '|' {
			seg := remark[p:i]
			if len(seg) > 5 && seg[:5] == "slot:" {
				var n int64
				for j := 5; j < len(seg); j++ {
					c := seg[j]
					if c < '0' || c > '9' {
						break
					}
					n = n*10 + int64(c-'0')
				}
				return n
			}
			p = i + 1
		}
	}
	if p < len(remark) {
		seg := remark[p:]
		if len(seg) > 5 && seg[:5] == "slot:" {
			var n int64
			for j := 5; j < len(seg); j++ {
				c := seg[j]
				if c < '0' || c > '9' {
					break
				}
				n = n*10 + int64(c-'0')
			}
			return n
		}
	}
	return -1
}

func parseItemCardIDFromRemark(remark string) int64 {
	// remark segments separated by '|', find segment starting with "itemcard:"
	p := 0
	for i := 0; i < len(remark); i++ {
		if remark[i] == '|' {
			seg := remark[p:i]
			if len(seg) > 9 && seg[:9] == "itemcard:" {
				var n int64
				for j := 9; j < len(seg); j++ {
					c := seg[j]
					if c < '0' || c > '9' {
						break
					}
					n = n*10 + int64(c-'0')
				}
				if n > 0 {
					return n
				}
			}
			p = i + 1
		}
	}
	// 检查最后一段
	if p < len(remark) {
		seg := remark[p:]
		if len(seg) > 9 && seg[:9] == "itemcard:" {
			var n int64
			for j := 9; j < len(seg); j++ {
				c := seg[j]
				if c < '0' || c > '9' {
					break
				}
				n = n*10 + int64(c-'0')
			}
			if n > 0 {
				return n
			}
		}
	}
	return 0
}
