package app

import (
	"bindbox-game/internal/code"
	"bindbox-game/internal/pkg/core"
	"bindbox-game/internal/pkg/validation"
	"bindbox-game/internal/repository/mysql/model"
	strat "bindbox-game/internal/service/activity/strategy"
	usersvc "bindbox-game/internal/service/user"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"math/rand"
	"net/http"
	"time"
)

type joinLotteryRequest struct {
	ActivityID int64   `json:"activity_id"`
	IssueID    int64   `json:"issue_id"`
	Count      int64   `json:"count"`
	Channel    string  `json:"channel"`
	SlotIndex  []int64 `json:"slot_index"`
	CouponID   *int64  `json:"coupon_id"`
	ItemCardID *int64  `json:"item_card_id"`
	UsePoints  *int64  `json:"use_points"`
}

type joinLotteryResponse struct {
	JoinID     string `json:"join_id"`
	OrderNo    string `json:"order_no"`
	Queued     bool   `json:"queued"`
	DrawMode   string `json:"draw_mode"`
	RewardID   int64  `json:"reward_id,omitempty"`
	RewardName string `json:"reward_name,omitempty"`
}

// JoinLottery 用户参与抽奖
// @Summary 用户参与抽奖
// @Description 提交活动ID与期ID创建参与记录与订单，支持积分抵扣，返回参与ID、订单号、抽奖模式及是否进入队列
// @Tags APP端.抽奖
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param RequestBody body joinLotteryRequest true "请求参数"
// @Success 200 {object} joinLotteryResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/lottery/join [post]
func (h *handler) JoinLottery() core.HandlerFunc {
	return func(ctx core.Context) {
		req := new(joinLotteryRequest)
		rsp := new(joinLotteryResponse)
		if err := ctx.ShouldBindJSON(req); err != nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
			return
		}
		userID := int64(ctx.SessionUserInfo().Id)
		activity, err := h.activity.GetActivity(ctx.RequestContext(), req.ActivityID)
		if err != nil || activity == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170001, "activity not found"))
			return
		}
		if !activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170009, "本活动不支持优惠券"))
			return
		}
		if !activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170010, "本活动不支持道具卡"))
			return
		}
		cfgMode := "scheduled"
		if activity.DrawMode != "" {
			cfgMode = activity.DrawMode
		}
		fmt.Printf("[抽奖下单] 用户=%d 活动ID=%d 期ID=%d 次数=%d 渠道=%s 优惠券ID=%v 道具卡ID=%v\n", userID, req.ActivityID, req.IssueID, req.Count, req.Channel, req.CouponID, req.ItemCardID)
		fmt.Printf("[抽奖下单] 活动票价(分)=%d 允许优惠券=%t 允许道具卡=%t 抽奖模式=%s 玩法=%s\n", activity.PriceDraw, activity.AllowCoupons, activity.AllowItemCards, cfgMode, activity.PlayType)
		// 定时一番赏：开奖前20秒禁止下单，防止订单抖动
		if activity.PlayType == "ichiban" && cfgMode == "scheduled" && !activity.ScheduledTime.IsZero() {
			now := time.Now()
			cutoff := activity.ScheduledTime.Add(-20 * time.Second)
			if now.After(cutoff) && now.Before(activity.ScheduledTime.Add(30*time.Second)) {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, 170012, "距离开奖时间不足20秒，暂停下单"))
				return
			}
		}
		if activity.PlayType == "ichiban" {
			if e := h.validateIchibanSlots(ctx, req); e != nil {
				ctx.AbortWithError(e)
				return
			}
		}
		joinID := h.randomID("J")
		orderNo := h.randomID("O")
		c := req.Count
		if c <= 0 {
			c = 1
		}
		total := activity.PriceDraw * c
		order := h.orderModel(userID, orderNo, total, req.ActivityID, req.IssueID, c)
		if len(req.SlotIndex) > 0 {
			order.Remark = fmt.Sprintf("lottery:activity:%d|issue:%d|count:%d|slots:%s", req.ActivityID, req.IssueID, c, buildSlotsRemarkWithScalarCount(req.SlotIndex))
		}
		if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
			if order.Remark == "" {
				order.Remark = fmt.Sprintf("lottery:activity:%d|issue:%d|count:%d", req.ActivityID, req.IssueID, c)
			}
			order.Remark = order.Remark + fmt.Sprintf("|itemcard:%d", *req.ItemCardID)
		}
		order.PointsAmount = 0
		order.PointsLedgerID = 0
		order.ActualAmount = order.TotalAmount
		fmt.Printf("[抽奖下单] 订单总额(分)=%d 初始实付(分)=%d 备注=%s\n", order.TotalAmount, order.ActualAmount, order.Remark)
		applied := int64(0)
		if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
			fmt.Printf("[抽奖下单] 尝试优惠券 用户券ID=%d 应用前实付(分)=%d 累计优惠(分)=%d\n", *req.CouponID, order.ActualAmount, order.DiscountAmount)
			applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
			fmt.Printf("[抽奖下单] 优惠后 实付(分)=%d 累计优惠(分)=%d 备注=%s\n", order.ActualAmount, order.DiscountAmount, order.Remark)
		}
		if req.UsePoints != nil && *req.UsePoints > 0 {
			bal, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
			usePts := *req.UsePoints
			if bal > 0 && usePts > bal {
				usePts = bal
			}
			ratePtsPerCent, _ := h.user.CentsToPoints(ctx.RequestContext(), 1)
			if ratePtsPerCent <= 0 {
				ratePtsPerCent = 1
			}
			deductCents := usePts / ratePtsPerCent
			if deductCents > order.ActualAmount {
				deductCents = order.ActualAmount
			}
			if deductCents > 0 {
				needPts := deductCents * ratePtsPerCent
				ledgerID, errConsume := h.user.ConsumePointsFor(ctx.RequestContext(), userID, needPts, "orders", orderNo, "order points consume", "consume_order")
				if errConsume != nil {
					ctx.AbortWithError(core.Error(http.StatusBadRequest, 170020, errConsume.Error()))
					return
				}
				order.PointsAmount = deductCents
				order.PointsLedgerID = ledgerID
				order.ActualAmount = order.ActualAmount - deductCents
			}
		}
		err = h.writeDB.Orders.WithContext(ctx.RequestContext()).Omit(h.writeDB.Orders.PaidAt, h.writeDB.Orders.CancelledAt).Create(order)

		if err != nil {
			ctx.AbortWithError(core.Error(http.StatusInternalServerError, 170002, err.Error()))
			return
		}
		// 写结构化优惠券使用明细（兼容保留 remark）
		if applied > 0 && req.CouponID != nil && *req.CouponID > 0 {
			_ = h.user.RecordOrderCouponUsage(ctx.RequestContext(), order.ID, *req.CouponID, applied)
		}
		// 优惠券扣减与核销在支付回调中执行（避免未支付时扣减）
		rsp.JoinID = joinID
		rsp.OrderNo = orderNo
		rsp.DrawMode = cfgMode
		fmt.Printf("[抽奖下单] 汇总 订单号=%s 总额(分)=%d 优惠(分)=%d 实付(分)=%d 队列=true 模式=%s\n", orderNo, order.TotalAmount, order.DiscountAmount, order.ActualAmount, cfgMode)
		if order.ActualAmount == 0 {
			now := time.Now()
			_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.OrderNo.Eq(orderNo)).Updates(map[string]any{h.writeDB.Orders.Status.ColumnName().String(): 2, h.writeDB.Orders.PaidAt.ColumnName().String(): now})
			// 0元订单：统一在“已支付”后扣券（解析 remark 中优惠券使用片段并扣减或核销）
			{
				ord, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(orderNo)).First()
				if ord != nil {
					parts := func(s string) []string {
						p := make([]string, 0, 8)
						b := 0
						for i := 0; i < len(s); i++ {
							if s[i] == '|' {
								if i > b {
									p = append(p, s[b:i])
								}
								b = i + 1
							}
						}
						if b < len(s) {
							p = append(p, s[b:])
						}
						return p
					}(ord.Remark)
					for _, seg := range parts {
						if len(seg) > 2 && seg[:2] == "c:" {
							// 解析 id 与 amount
							j := 2
							var cid int64
							for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' {
								cid = cid*10 + int64(seg[j]-'0')
								j++
							}
							var applied int64
							if j < len(seg) && seg[j] == ':' {
								j++
								for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' {
									applied = applied*10 + int64(seg[j]-'0')
									j++
								}
							}
							uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(cid), h.readDB.UserCoupons.UserID.Eq(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()
								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
										nb := bal - applied
										if nb < 0 {
											nb = 0
										}
										if nb == 0 {
											_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(uc.ID)).Updates(map[string]any{"balance_amount": nb, h.readDB.UserCoupons.Status.ColumnName().String(): 2, h.readDB.UserCoupons.UsedOrderID.ColumnName().String(): ord.ID, h.readDB.UserCoupons.UsedAt.ColumnName().String(): now})
										} else {
											_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(uc.ID)).Updates(map[string]any{"balance_amount": nb, h.readDB.UserCoupons.UsedOrderID.ColumnName().String(): ord.ID, h.readDB.UserCoupons.UsedAt.ColumnName().String(): now})
										}
									} 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(): now})
									}
								}
							}
						}
					}
				}
			}
			if cfgMode == "instant" {
				ord, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(orderNo)).First()
				if ord != nil {
					// 解析次数
					slotsIdx, slotsCnt := parseSlotsCountsFromRemark(ord.Remark)
					dc := func() int64 {
						if len(slotsIdx) > 0 && len(slotsIdx) == len(slotsCnt) {
							var s int64
							for i := range slotsCnt {
								if slotsCnt[i] > 0 {
									s += slotsCnt[i]
								}
							}
							if s > 0 {
								return s
							}
						}
						remark := ord.Remark
						p := 0
						for i := 0; i < len(remark); i++ {
							if remark[i] == '|' {
								seg := remark[p:i]
								if len(seg) > 6 && seg[:6] == "count:" {
									var n int64
									for j := 6; j < len(seg); j++ {
										c := seg[j]
										if c < '0' || c > '9' {
											break
										}
										n = n*10 + int64(c-'0')
									}
									if n <= 0 {
										return 1
									}
									return n
								}
								p = i + 1
							}
						}
						return 1
					}()
					sel := strat.NewDefault(h.readDB, h.writeDB)
					logs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(ord.ID)).Find()
					done := int64(len(logs))
					rem := make([]int64, len(slotsCnt))
					copy(rem, slotsCnt)
					cur := 0
					for i := done; i < dc; i++ {
						rid := int64(0)
						var e2 error
						if activity.PlayType == "ichiban" {
							slot := func() int64 {
								if len(slotsIdx) > 0 && len(slotsIdx) == len(rem) {
									for cur < len(rem) && rem[cur] == 0 {
										cur++
									}
									if cur >= len(rem) {
										return -1
									}
									rem[cur]--
									return slotsIdx[cur] - 1
								}
								return parseSlotFromRemark(ord.Remark)
							}()
							if slot >= 0 {
								var cnt int64
								_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index=?", req.IssueID, slot).Scan(&cnt).Error
								if cnt > 0 {
									break
								}
								e := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", req.IssueID, slot, userID, ord.ID).Error
								if e != nil {
									break
								}
								rid, _, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), req.ActivityID, req.IssueID, slot)
							} else {
								rid, _, e2 = sel.SelectItem(ctx.RequestContext(), req.ActivityID, req.IssueID, userID)
							}
						} else {
							rid, _, e2 = sel.SelectItem(ctx.RequestContext(), req.ActivityID, req.IssueID, 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
						}
						if activity.PlayType == "ichiban" {
							_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
						} else {
							_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &req.ActivityID, RewardID: &rid, Remark: rw.Name})
						}
						// 创建抽奖日志（获取ID用于道具卡核销）
						log := &model.ActivityDrawLogs{UserID: userID, IssueID: req.IssueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
						_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
						// 道具卡效果（奖励倍数/概率提升的简单实现：奖励倍数=额外发同奖品；概率提升=尝试升级到更高等级）
						fmt.Printf("[道具卡-JoinLottery] 开始检查 活动允许道具卡=%t 请求道具卡ID=%v\n", activity.AllowItemCards, req.ItemCardID)
						if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
							fmt.Printf("[道具卡-JoinLottery] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *req.ItemCardID)
							uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*req.ItemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).First()
							if uic != nil {
								fmt.Printf("[道具卡-JoinLottery] 找到用户道具卡 ID=%d CardID=%d Status=%d ValidStart=%s ValidEnd=%s\n", uic.ID, uic.CardID, uic.Status, uic.ValidStart.Format(time.RFC3339), uic.ValidEnd.Format(time.RFC3339))
								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 {
									fmt.Printf("[道具卡-JoinLottery] 找到系统道具卡 ID=%d Name=%s EffectType=%d RewardMultiplierX1000=%d ScopeType=%d ActivityID=%d IssueID=%d Status=%d\n", ic.ID, ic.Name, ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, ic.ActivityID, ic.IssueID, ic.Status)
									fmt.Printf("[道具卡-JoinLottery] 时间检查 当前时间=%s ValidStart.After(now)=%t ValidEnd.Before(now)=%t\n", now.Format(time.RFC3339), uic.ValidStart.After(now), uic.ValidEnd.Before(now))
									if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
										scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == req.ActivityID) || (ic.ScopeType == 4 && ic.IssueID == req.IssueID)
										fmt.Printf("[道具卡-JoinLottery] 范围检查 ScopeType=%d 请求ActivityID=%d 请求IssueID=%d scopeOK=%t\n", ic.ScopeType, req.ActivityID, req.IssueID, scopeOK)
										if scopeOK {
											if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 { // ×2及以上：额外发一次相同奖品
												fmt.Printf("[道具卡-JoinLottery] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s PlayType=%s\n", ic.RewardMultiplierX1000, rid, rw.Name, activity.PlayType)
												if activity.PlayType == "ichiban" {
													_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
												} else {
													_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &req.ActivityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
												}
												fmt.Printf("[道具卡-JoinLottery] ✅ 双倍奖励发放完成\n")
											} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 { // 概率提升：尝试升级到更高等级的可用奖品
												fmt.Printf("[道具卡-JoinLottery] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
												uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(req.IssueID)).Find()
												var better *model.ActivityRewardSettings
												for _, r := range uprw {
													if r.Level < rw.Level && r.Quantity != 0 {
														if better == nil || r.Level < better.Level {
															better = r
														}
													}
												}
												if better != nil {
													// 以boost率决定升级
													if rand.Int31n(1000) < ic.BoostRateX1000 {
														rid2 := better.ID
														fmt.Printf("[道具卡-JoinLottery] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
														if activity.PlayType == "ichiban" {
															_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid2)
														} else {
															_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: better.ProductID, Quantity: 1, ActivityID: &req.ActivityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
														}
														_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: req.IssueID, OrderID: ord.ID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1})
													} else {
														fmt.Printf("[道具卡-JoinLottery] 概率提升未触发\n")
													}
												} else {
													fmt.Printf("[道具卡-JoinLottery] 没有找到更好的奖品可升级\n")
												}
											} else {
												fmt.Printf("[道具卡-JoinLottery] ⚠️ 道具卡效果类型不匹配或倍数不足 EffectType=%d RewardMultiplierX1000=%d\n", ic.EffectType, ic.RewardMultiplierX1000)
											}
											// 核销道具卡
											fmt.Printf("[道具卡-JoinLottery] 核销道具卡 用户道具卡ID=%d\n", *req.ItemCardID)
											_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*req.ItemCardID), h.readDB.UserItemCards.UserID.Eq(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(): req.ActivityID,
												h.readDB.UserItemCards.UsedIssueID.ColumnName().String():    req.IssueID,
												h.readDB.UserItemCards.UsedAt.ColumnName().String():         time.Now(),
											})
										} else {
											fmt.Printf("[道具卡-JoinLottery] ❌ 范围检查失败\n")
										}
									} else {
										fmt.Printf("[道具卡-JoinLottery] ❌ 时间检查失败\n")
									}
								} else {
									fmt.Printf("[道具卡-JoinLottery] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
								}
							} else {
								fmt.Printf("[道具卡-JoinLottery] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *req.ItemCardID)
							}
						} else {
							fmt.Printf("[道具卡-JoinLottery] 跳过道具卡检查\n")
						}
					}
				}
			}
			rsp.Queued = true
		} else {
			rsp.Queued = true
		}
		ctx.Payload(rsp)
	}
}

type resultQueryRequest struct {
	JoinID  string `form:"join_id"`
	OrderID int64  `form:"order_id"`
}

type resultResponse struct {
	Result  map[string]any `json:"result"`
	Receipt map[string]any `json:"receipt"`
}

// GetLotteryResult 抽奖结果查询
// @Summary 抽奖结果查询
// @Description 根据参与ID与期ID查询抽奖结果；即时模式会立即开奖并发奖；同时返回服务端签名凭证用于校验
// @Tags APP端.抽奖
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param join_id query string true "参与ID"
// @Param order_id query int false "订单ID，用于区分同一用户多次参与"
// @Param issue_id query int false "期ID"
// @Success 200 {object} resultResponse
// @Failure 400 {object} code.Failure
func (h *handler) GetLotteryResult() core.HandlerFunc {
	return func(ctx core.Context) {
		req := new(resultQueryRequest)
		rsp := new(resultResponse)
		if err := ctx.ShouldBindQuery(req); err != nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
			return
		}
		issueIDStr := ctx.RequestInputParams().Get("issue_id")
		var issueID int64
		if issueIDStr != "" {
			for i := 0; i < len(issueIDStr); i++ {
				c := issueIDStr[i]
				if c < '0' || c > '9' {
					issueID = 0
					break
				}
				issueID = issueID*10 + int64(c-'0')
			}
		}
		var orderID int64
		var ord *model.Orders
		if req.OrderID > 0 {
			orderID = req.OrderID
			ord, _ = h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(orderID)).First()
		}
		userID := int64(ctx.SessionUserInfo().Id)
		issue, _ := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityIssues.ID.Eq(issueID)).First()
		activityID := func() int64 {
			if issue != nil {
				return issue.ActivityID
			}
			return 0
		}()
		actCommit, err := func() (*model.Activities, error) {
			if activityID <= 0 {
				return nil, nil
			}
			return h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(activityID)).First()
		}()
		if err != nil || actCommit == nil || len(actCommit.CommitmentSeedMaster) == 0 {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170003, "commitment not found"))
			return
		}
		cfgMode := "scheduled"
		if act, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(activityID)).First(); act != nil && act.DrawMode != "" {
			cfgMode = act.DrawMode
		}
		// 结果优先返回历史抽奖日志
		var existed *model.ActivityDrawLogs
		if orderID > 0 {
			existed, _ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(orderID)).First()
		} else {
			existed, _ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.UserID.Eq(userID), h.readDB.ActivityDrawLogs.IssueID.Eq(issueID)).First()
		}
		if existed != nil && existed.RewardID > 0 {
			rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(existed.RewardID)).First()
			rsp.Result = map[string]any{"reward_id": existed.RewardID, "reward_name": func() string {
				if rw != nil {
					return rw.Name
				}
				return ""
			}()}
		} else if cfgMode == "instant" {
			// 即时开奖必须绑定订单且校验归属与支付状态
			if orderID == 0 || ord == nil {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, 170005, "order required for instant draw"))
				return
			}
			if ord.UserID != userID {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, 170005, "order not owned by user"))
				return
			}
			if ord.Status != 2 {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, 170006, "order not paid"))
				return
			}
			if actCommit.PlayType == "ichiban" {
				slot := parseSlotFromRemark(ord.Remark)
				if slot >= 0 {
					if err := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", issueID, slot, userID, orderID).Error; err == nil {
						rid, _, e2 := strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), activityID, issueID, slot)
						if e2 == nil && rid > 0 {
							rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
							if rw != nil {
								_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
								_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1})
								rsp.Result = map[string]any{"reward_id": rid, "reward_name": rw.Name}
							}
						}
					}
				}
			} else {
				sel := strat.NewDefault(h.readDB, h.writeDB)
				rid, _, e2 := sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
				if e2 == nil && rid > 0 {
					rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
					_ = sel.GrantReward(ctx.RequestContext(), userID, rid)
					log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid, IsWinner: 1, Level: func() int32 {
						if rw != nil {
							return rw.Level
						}
						return 1
					}(), CurrentLevel: 1}
					_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
					icID := parseItemCardIDFromRemark(ord.Remark)
					fmt.Printf("[道具卡-GetLotteryResult] 从订单备注解析道具卡ID icID=%d 订单备注=%s\n", icID, ord.Remark)
					if icID > 0 {
						fmt.Printf("[道具卡-GetLotteryResult] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, icID)
						uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(icID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).First()
						if uic != nil {
							fmt.Printf("[道具卡-GetLotteryResult] 找到用户道具卡 ID=%d CardID=%d Status=%d ValidStart=%s ValidEnd=%s\n", uic.ID, uic.CardID, uic.Status, uic.ValidStart.Format(time.RFC3339), uic.ValidEnd.Format(time.RFC3339))
							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 {
								fmt.Printf("[道具卡-GetLotteryResult] 找到系统道具卡 ID=%d Name=%s EffectType=%d RewardMultiplierX1000=%d ScopeType=%d ActivityID=%d IssueID=%d Status=%d\n", ic.ID, ic.Name, ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, ic.ActivityID, ic.IssueID, ic.Status)
								fmt.Printf("[道具卡-GetLotteryResult] 时间检查 当前时间=%s ValidStart.After(now)=%t ValidEnd.Before(now)=%t\n", now.Format(time.RFC3339), uic.ValidStart.After(now), uic.ValidEnd.Before(now))
								if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
									scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == activityID) || (ic.ScopeType == 4 && ic.IssueID == issueID)
									fmt.Printf("[道具卡-GetLotteryResult] 范围检查 ScopeType=%d ActivityID=%d IssueID=%d scopeOK=%t\n", ic.ScopeType, activityID, issueID, scopeOK)
									if scopeOK {
										eff := &model.ActivityDrawEffects{
											DrawLogID:             log.ID,
											UserID:                userID,
											UserItemCardID:        uic.ID,
											SystemItemCardID:      ic.ID,
											Applied:               1,
											CardType:              ic.CardType,
											EffectType:            ic.EffectType,
											RewardMultiplierX1000: ic.RewardMultiplierX1000,
											ProbabilityDeltaX1000: ic.BoostRateX1000,
											ScopeType:             ic.ScopeType,
											ActivityCategoryID:    actCommit.ActivityCategoryID,
											ActivityID:            activityID,
											IssueID:               issueID,
										}
										_ = h.writeDB.ActivityDrawEffects.WithContext(ctx.RequestContext()).Create(eff)
										fmt.Printf("[道具卡-GetLotteryResult] 创建道具卡效果记录 EffectID=%d\n", eff.ID)
										if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
											fmt.Printf("[道具卡-GetLotteryResult] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s\n", ic.RewardMultiplierX1000, rid, rw.Name)
											_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: orderID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
											fmt.Printf("[道具卡-GetLotteryResult] ✅ 双倍奖励发放完成\n")
										} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
											fmt.Printf("[道具卡-GetLotteryResult] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
											uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
											var better *model.ActivityRewardSettings
											for _, r := range uprw {
												if r.Level < rw.Level && r.Quantity != 0 {
													if better == nil || r.Level < better.Level {
														better = r
													}
												}
											}
											if better != nil && rand.Int31n(1000) < ic.BoostRateX1000 {
												rid2 := better.ID
												fmt.Printf("[道具卡-GetLotteryResult] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
												_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: orderID, ProductID: better.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
												_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: orderID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1})
											} else {
												fmt.Printf("[道具卡-GetLotteryResult] 概率提升未触发\n")
											}
										} else {
											fmt.Printf("[道具卡-GetLotteryResult] ⚠️ 道具卡效果类型不匹配或倍数不足 EffectType=%d RewardMultiplierX1000=%d\n", ic.EffectType, ic.RewardMultiplierX1000)
										}
										fmt.Printf("[道具卡-GetLotteryResult] 核销道具卡 用户道具卡ID=%d\n", icID)
										_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(icID), h.readDB.UserItemCards.UserID.Eq(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(): activityID, h.readDB.UserItemCards.UsedIssueID.ColumnName().String(): issueID, h.readDB.UserItemCards.UsedAt.ColumnName().String(): time.Now()})
									} else {
										fmt.Printf("[道具卡-GetLotteryResult] ❌ 范围检查失败\n")
									}
								} else {
									fmt.Printf("[道具卡-GetLotteryResult] ❌ 时间检查失败\n")
								}
							} else {
								fmt.Printf("[道具卡-GetLotteryResult] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
							}
						} else {
							fmt.Printf("[道具卡-GetLotteryResult] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, icID)
						}
					} else {
						fmt.Printf("[道具卡-GetLotteryResult] 订单备注中没有道具卡ID\n")
					}
					rsp.Result = map[string]any{"reward_id": rid, "reward_name": func() string {
						if rw != nil {
							return rw.Name
						}
						return ""
					}()}
				}
			}
		}
		ts := time.Now().UnixMilli()
		nonce := rand.Int63()
		mac := hmac.New(sha256.New, actCommit.CommitmentSeedMaster)
		mac.Write([]byte(h.joinSigPayload(userID, issueID, ts, nonce)))
		sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
		if rsp.Result == nil {
			rsp.Result = map[string]any{}
		}
		rsp.Receipt = map[string]any{
			"issue_id":     issueID,
			"seed_version": actCommit.CommitmentStateVersion,
			"timestamp":    ts,
			"nonce":        nonce,
			"signature":    sig,
			"algorithm":    "HMAC-SHA256",
			"inputs":       map[string]any{"user_id": userID, "issue_id": issueID, "order_id": orderID, "timestamp": ts, "nonce": nonce},
		}
		ctx.Payload(rsp)
	}
}

func (h *handler) randomID(prefix string) string {
	now := time.Now()
	return prefix + now.Format("20060102150405")
}

func (h *handler) orderModel(userID int64, orderNo string, amount int64, activityID int64, issueID int64, count int64) *model.Orders {
	return &model.Orders{UserID: userID, OrderNo: orderNo, SourceType: 2, TotalAmount: amount, DiscountAmount: 0, PointsAmount: 0, ActualAmount: amount, Status: 1, IsConsumed: 0, Remark: fmt.Sprintf("lottery:activity:%d|issue:%d|count:%d", activityID, issueID, count)}
}

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
		}
	}
	return 0
}

func (h *handler) joinSigPayload(userID int64, issueID int64, ts int64, nonce int64) string {
	return fmt.Sprintf("%d|%d|%d|%d", userID, issueID, ts, nonce)
}

func buildSlotsRemarkWithScalarCount(slots []int64) string {
	s := ""
	for i := range slots {
		if i > 0 {
			s += ","
		}
		s += fmt.Sprintf("%d:%d", slots[i]-1, 1)
	}
	return s
}

func parseSlotsCountsFromRemark(remark string) ([]int64, []int64) {
	if remark == "" {
		return nil, nil
	}
	p := 0
	for i := 0; i < len(remark); i++ {
		if remark[i] == '|' {
			seg := remark[p:i]
			if len(seg) > 6 && seg[:6] == "slots:" {
				pairs := seg[6:]
				idxs := make([]int64, 0)
				cnts := make([]int64, 0)
				start := 0
				for start <= len(pairs) {
					end := start
					for end < len(pairs) && pairs[end] != ',' {
						end++
					}
					if end > start {
						a := pairs[start:end]
						// a format: num:num
						x, y := int64(0), int64(0)
						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
			}
			p = i + 1
		}
	}
	return nil, nil
}

// validateIchibanSlots 一番赏格位校验
// 功能：校验请求中的格位选择是否有效（数量匹配、范围合法、未被占用）
// 参数：
//   - ctx：请求上下文
//   - req：JoinLottery 请求体，使用其中的 issue_id、count、slot_index
//
// 返回：core.BusinessError 用于直接传递给 AbortWithError；合法时返回 nil
func (h *handler) validateIchibanSlots(ctx core.Context, req *joinLotteryRequest) core.BusinessError {
	var totalSlots int64
	_ = h.repo.GetDbR().Raw("SELECT COALESCE(SUM(original_qty),0) FROM activity_reward_settings WHERE issue_id=?", req.IssueID).Scan(&totalSlots).Error
	if totalSlots <= 0 {
		return core.Error(http.StatusBadRequest, 170008, "no slots")
	}
	if len(req.SlotIndex) > 0 {
		if req.Count <= 0 || req.Count != int64(len(req.SlotIndex)) {
			return core.Error(http.StatusBadRequest, code.ParamBindError, "参数错误")
		}
		seen := make(map[int64]struct{})
		for i := range req.SlotIndex {
			si := req.SlotIndex[i]
			if _, ok := seen[si]; ok {
				return core.Error(http.StatusBadRequest, 170011, "duplicate slots not allowed")
			}
			seen[si] = struct{}{}
			if si < 1 || si > totalSlots {
				return core.Error(http.StatusBadRequest, 170008, "slot out of range")
			}
			var cnt int64
			_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index=?", req.IssueID, si-1).Scan(&cnt).Error
			if cnt > 0 {
				return core.Error(http.StatusBadRequest, 170007, "位置已被占用")
			}
		}
	}
	return nil
}

// applyCouponWithCap 优惠券抵扣（含50%封顶与金额券部分使用）
// 功能：在订单上应用一张用户券，实施总价50%封顶；金额券支持“部分使用”，在 remark 记录明细
// 参数：
//   - ctx：请求上下文
//   - userID：用户ID
//   - order：待更新的订单对象（入参引用，被本函数更新 discount_amount/actual_amount/remark）
//   - activityID：活动ID用于范围校验
//   - userCouponID：用户持券ID
//
// 返回：本次实际应用的抵扣金额（分）；若不适用或受封顶为0则返回0
func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *model.Orders, activityID int64, userCouponID int64) int64 {
	uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(userCouponID), h.readDB.UserCoupons.UserID.Eq(userID), h.readDB.UserCoupons.Status.Eq(1)).First()
	if uc == nil {
		return 0
	}
	fmt.Printf("[优惠券] 用户券ID=%d 开始=%s 结束=%s\n", userCouponID, uc.ValidStart.Format(time.RFC3339), func() string {
		if uc.ValidEnd.IsZero() {
			return "无截止"
		}
		return uc.ValidEnd.Format(time.RFC3339)
	}())
	sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID), h.readDB.SystemCoupons.Status.Eq(1)).First()
	now := time.Now()
	if sc == nil {
		fmt.Printf("[优惠券] 模板不存在 用户券ID=%d\n", userCouponID)
		return 0
	}
	if uc.ValidStart.After(now) {
		fmt.Printf("[优惠券] 未到开始时间 用户券ID=%d 当前=%s\n", userCouponID, now.Format(time.RFC3339))
		return 0
	}
	if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) {
		fmt.Printf("[优惠券] 已过期 用户券ID=%d 当前=%s\n", userCouponID, now.Format(time.RFC3339))
		return 0
	}
	scopeOK := (sc.ScopeType == 1) || (sc.ScopeType == 2 && sc.ActivityID == activityID)
	if !scopeOK {
		fmt.Printf("[优惠券] 范围不匹配 用户券ID=%d scope_type=%d 模板活动ID=%d 当前活动ID=%d\n", userCouponID, sc.ScopeType, sc.ActivityID, activityID)
		return 0
	}
	if order.TotalAmount < sc.MinSpend {
		fmt.Printf("[优惠券] 未达使用门槛 用户券ID=%d min_spend(分)=%d 订单总额(分)=%d\n", userCouponID, sc.MinSpend, order.TotalAmount)
		return 0
	}
	cap := order.TotalAmount / 2
	remainingCap := cap - order.DiscountAmount
	if remainingCap <= 0 {
		fmt.Printf("[优惠券] 已达封顶 封顶(分)=%d 剩余封顶(分)=0\n", cap)
		return 0
	}
	fmt.Printf("[优惠券] 计算前 类型=%d 面值/折扣=%d 封顶(分)=%d 剩余封顶(分)=%d 当前实付(分)=%d 累计优惠(分)=%d\n", sc.DiscountType, sc.DiscountValue, cap, remainingCap, order.ActualAmount, order.DiscountAmount)
	applied := int64(0)
	switch sc.DiscountType {
	case 1:
		var bal int64
		_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
		if bal <= 0 {
			bal = sc.DiscountValue
		}
		fmt.Printf("[优惠券] 金额券余额(分)=%d 模板面值(分)=%d\n", bal, sc.DiscountValue)
		if bal > 0 {
			if bal > remainingCap {
				applied = remainingCap
			} else {
				applied = bal
			}
		}
	case 2:
		applied = sc.DiscountValue
		if applied > remainingCap {
			applied = remainingCap
		}
		fmt.Printf("[优惠券] 满减券 应用金额(分)=%d\n", applied)
	case 3:
		rate := sc.DiscountValue
		if rate < 0 {
			rate = 0
		}
		if rate > 1000 {
			rate = 1000
		}
		newAmt := order.ActualAmount * rate / 1000
		d := order.ActualAmount - newAmt
		if d > remainingCap {
			applied = remainingCap
		} else {
			applied = d
		}
		fmt.Printf("[优惠券] 折扣券 折扣千分比=%d 抵扣(分)=%d\n", rate, applied)
	}
	if applied > order.ActualAmount {
		applied = order.ActualAmount
	}
	fmt.Printf("[优惠券] 本次抵扣(分)=%d\n", applied)
	if applied <= 0 {
		return 0
	}
	order.DiscountAmount += applied
	order.ActualAmount -= applied
	order.Remark = order.Remark + fmt.Sprintf("|c:%d:%d", userCouponID, applied)
	fmt.Printf("[优惠券] 应用后 累计优惠(分)=%d 订单实付(分)=%d\n", order.DiscountAmount, order.ActualAmount)
	return applied
}

// updateUserCouponAfterApply 应用后更新用户券（扣减余额或核销）
// 功能：根据订单 remark 中记录的 applied_amount，
//
//	对直金额券扣减余额并在余额为0时核销；满减/折扣券一次性核销
//
// 参数：
//   - ctx：请求上下文
//   - userID：用户ID
//   - order：订单（用于读取 remark 和写入 used_order_id）
//   - userCouponID：用户持券ID
//
// 返回：无
func (h *handler) updateUserCouponAfterApply(ctx core.Context, userID int64, order *model.Orders, userCouponID int64) {
	uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(userCouponID), h.readDB.UserCoupons.UserID.Eq(userID), h.readDB.UserCoupons.Status.Eq(1)).First()
	if uc == nil {
		return
	}
	sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID), h.readDB.SystemCoupons.Status.Eq(1)).First()
	if sc == nil {
		return
	}
	applied := int64(0)
	remark := order.Remark
	p := 0
	for i := 0; i < len(remark); i++ {
		if remark[i] == '|' {
			seg := remark[p:i]
			if len(seg) > 2 && seg[:2] == "c:" {
				j := 2
				var id int64
				for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' {
					id = id*10 + int64(seg[j]-'0')
					j++
				}
				if j < len(seg) && seg[j] == ':' {
					j++
					var amt int64
					for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' {
						amt = amt*10 + int64(seg[j]-'0')
						j++
					}
					if id == userCouponID {
						applied = amt
					}
				}
			}
			p = i + 1
		}
	}
	if sc.DiscountType == 1 {
		var bal int64
		_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
		newBal := bal - applied
		if newBal < 0 {
			newBal = 0
		}
		if newBal == 0 {
			_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.UserCoupons.ID.Eq(userCouponID), h.writeDB.UserCoupons.UserID.Eq(userID)).Updates(map[string]any{"balance_amount": newBal, h.writeDB.UserCoupons.Status.ColumnName().String(): 2, h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, h.writeDB.UserCoupons.UsedAt.ColumnName().String(): time.Now()})
		} else {
			_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.UserCoupons.ID.Eq(userCouponID), h.writeDB.UserCoupons.UserID.Eq(userID)).Updates(map[string]any{"balance_amount": newBal, h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, h.writeDB.UserCoupons.UsedAt.ColumnName().String(): time.Now()})
		}
	} else {
		_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.UserCoupons.ID.Eq(userCouponID), h.writeDB.UserCoupons.UserID.Eq(userID)).Updates(map[string]any{h.writeDB.UserCoupons.Status.ColumnName().String(): 2, h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, h.writeDB.UserCoupons.UsedAt.ColumnName().String(): time.Now()})
	}
}

// markOrderPaid 将订单标记为已支付
// 功能：用于0元订单直接置为已支付并写入支付时间
// 参数：
//   - ctx：请求上下文
//   - orderNo：订单号
//
// 返回：无
func (h *handler) markOrderPaid(ctx core.Context, orderNo string) {
	now := time.Now()
	_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.OrderNo.Eq(orderNo)).Updates(map[string]any{h.writeDB.Orders.Status.ColumnName().String(): 2, h.writeDB.Orders.PaidAt.ColumnName().String(): now})
}

// processInstantDraw 即时抽奖流程
// 功能：在订单已支付情况下，执行抽奖与发奖；支持一番赏固定格位与普通模式，处理道具卡效果
// 参数：
//   - ctx：请求上下文
//   - userID：用户ID
//   - activity：活动实体（用于判断玩法）
//   - activityID：活动ID
//   - issueID：期ID
//   - orderNo：订单号
//   - itemCardID：道具卡ID（可选）
//
// 返回：无
func (h *handler) processInstantDraw(ctx core.Context, userID int64, activity *model.Activities, activityID int64, issueID int64, orderNo string, itemCardID *int64) {
	ord, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(orderNo)).First()
	if ord == nil {
		return
	}
	slotsIdx, slotsCnt := parseSlotsCountsFromRemark(ord.Remark)
	dc := func() int64 {
		if len(slotsIdx) > 0 && len(slotsIdx) == len(slotsCnt) {
			var s int64
			for i := range slotsCnt {
				if slotsCnt[i] > 0 {
					s += slotsCnt[i]
				}
			}
			if s > 0 {
				return s
			}
		}
		remark := ord.Remark
		p := 0
		for i := 0; i < len(remark); i++ {
			if remark[i] == '|' {
				seg := remark[p:i]
				if len(seg) > 6 && seg[:6] == "count:" {
					var n int64
					for j := 6; j < len(seg); j++ {
						c := seg[j]
						if c < '0' || c > '9' {
							break
						}
						n = n*10 + int64(c-'0')
					}
					if n <= 0 {
						return 1
					}
					return n
				}
				p = i + 1
			}
		}
		return 1
	}()
	sel := strat.NewDefault(h.readDB, h.writeDB)
	logs, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(ord.ID)).Find()
	done := int64(len(logs))
	rem := make([]int64, len(slotsCnt))
	copy(rem, slotsCnt)
	cur := 0
	for i := done; i < dc; i++ {
		rid := int64(0)
		var e2 error
		if activity.PlayType == "ichiban" {
			slot := func() int64 {
				if len(slotsIdx) > 0 && len(slotsIdx) == len(rem) {
					for cur < len(rem) && rem[cur] == 0 {
						cur++
					}
					if cur >= len(rem) {
						return -1
					}
					rem[cur]--
					return slotsIdx[cur] - 1
				}
				return parseSlotFromRemark(ord.Remark)
			}()
			if slot >= 0 {
				var cnt int64
				_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index=?", issueID, slot).Scan(&cnt).Error
				if cnt > 0 {
					break
				}
				e := h.repo.GetDbW().Exec("INSERT INTO issue_position_claims (issue_id, slot_index, user_id, order_id, created_at) VALUES (?,?,?,?,NOW(3))", issueID, slot, userID, ord.ID).Error
				if e != nil {
					break
				}
				rid, _, e2 = strat.NewIchiban(h.readDB, h.writeDB).SelectItemBySlot(ctx.RequestContext(), activityID, issueID, slot)
			} else {
				rid, _, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, userID)
			}
		} else {
			rid, _, e2 = sel.SelectItem(ctx.RequestContext(), activityID, issueID, 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
		}
		if activity.PlayType == "ichiban" {
			_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
		} else {
			_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name})
		}
		log := &model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid, IsWinner: 1, Level: rw.Level, CurrentLevel: 1}
		_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(log)
		fmt.Printf("[道具卡-processInstantDraw] 开始检查 活动允许道具卡=%t itemCardID=%v\n", activity.AllowItemCards, itemCardID)
		if activity.AllowItemCards && itemCardID != nil && *itemCardID > 0 {
			fmt.Printf("[道具卡-processInstantDraw] 查询用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
			uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*itemCardID), h.readDB.UserItemCards.UserID.Eq(userID), h.readDB.UserItemCards.Status.Eq(1)).First()
			if uic != nil {
				fmt.Printf("[道具卡-processInstantDraw] 找到用户道具卡 ID=%d CardID=%d Status=%d ValidStart=%s ValidEnd=%s\n", uic.ID, uic.CardID, uic.Status, uic.ValidStart.Format(time.RFC3339), uic.ValidEnd.Format(time.RFC3339))
				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 {
					fmt.Printf("[道具卡-processInstantDraw] 找到系统道具卡 ID=%d Name=%s EffectType=%d RewardMultiplierX1000=%d ScopeType=%d ActivityID=%d IssueID=%d Status=%d\n", ic.ID, ic.Name, ic.EffectType, ic.RewardMultiplierX1000, ic.ScopeType, ic.ActivityID, ic.IssueID, ic.Status)
					fmt.Printf("[道具卡-processInstantDraw] 时间检查 当前时间=%s ValidStart.After(now)=%t ValidEnd.Before(now)=%t\n", now.Format(time.RFC3339), uic.ValidStart.After(now), uic.ValidEnd.Before(now))
					if !uic.ValidStart.After(now) && !uic.ValidEnd.Before(now) {
						scopeOK := (ic.ScopeType == 1) || (ic.ScopeType == 3 && ic.ActivityID == activityID) || (ic.ScopeType == 4 && ic.IssueID == issueID)
						fmt.Printf("[道具卡-processInstantDraw] 范围检查 ScopeType=%d ActivityID=%d IssueID=%d scopeOK=%t\n", ic.ScopeType, activityID, issueID, scopeOK)
						if scopeOK {
							if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
								fmt.Printf("[道具卡-processInstantDraw] ✅ 应用双倍奖励 倍数=%d 奖品ID=%d 奖品名=%s PlayType=%s\n", ic.RewardMultiplierX1000, rid, rw.Name, activity.PlayType)
								if activity.PlayType == "ichiban" {
									_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid)
								} else {
									_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: rw.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid, Remark: rw.Name + "(倍数)"})
								}
								fmt.Printf("[道具卡-processInstantDraw] ✅ 双倍奖励发放完成\n")
							} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
								fmt.Printf("[道具卡-processInstantDraw] 应用概率提升 BoostRateX1000=%d\n", ic.BoostRateX1000)
								uprw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
								var better *model.ActivityRewardSettings
								for _, r := range uprw {
									if r.Level < rw.Level && r.Quantity != 0 {
										if better == nil || r.Level < better.Level {
											better = r
										}
									}
								}
								if better != nil && rand.Int31n(1000) < ic.BoostRateX1000 {
									rid2 := better.ID
									fmt.Printf("[道具卡-processInstantDraw] ✅ 概率提升成功 升级到奖品ID=%d 奖品名=%s\n", rid2, better.Name)
									if activity.PlayType == "ichiban" {
										_ = strat.NewIchiban(h.readDB, h.writeDB).GrantReward(ctx.RequestContext(), userID, rid2)
									} else {
										_, _ = h.user.GrantRewardToOrder(ctx.RequestContext(), userID, usersvc.GrantRewardToOrderRequest{OrderID: ord.ID, ProductID: better.ProductID, Quantity: 1, ActivityID: &activityID, RewardID: &rid2, Remark: better.Name + "(升级)"})
									}
									_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(&model.ActivityDrawLogs{UserID: userID, IssueID: issueID, OrderID: ord.ID, RewardID: rid2, IsWinner: 1, Level: better.Level, CurrentLevel: 1})
								} else {
									fmt.Printf("[道具卡-processInstantDraw] 概率提升未触发\n")
								}
							} else {
								fmt.Printf("[道具卡-processInstantDraw] ⚠️ 道具卡效果类型不匹配或倍数不足 EffectType=%d RewardMultiplierX1000=%d\n", ic.EffectType, ic.RewardMultiplierX1000)
							}
							fmt.Printf("[道具卡-processInstantDraw] 核销道具卡 用户道具卡ID=%d\n", *itemCardID)
							_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(h.readDB.UserItemCards.ID.Eq(*itemCardID), h.readDB.UserItemCards.UserID.Eq(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(): activityID, h.readDB.UserItemCards.UsedIssueID.ColumnName().String(): issueID, h.readDB.UserItemCards.UsedAt.ColumnName().String(): time.Now()})
						} else {
							fmt.Printf("[道具卡-processInstantDraw] ❌ 范围检查失败\n")
						}
					} else {
						fmt.Printf("[道具卡-processInstantDraw] ❌ 时间检查失败\n")
					}
				} else {
					fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到系统道具卡 CardID=%d\n", uic.CardID)
				}
			} else {
				fmt.Printf("[道具卡-processInstantDraw] ❌ 未找到用户道具卡 用户ID=%d 道具卡ID=%d\n", userID, *itemCardID)
			}
		} else {
			fmt.Printf("[道具卡-processInstantDraw] 跳过道具卡检查\n")
		}
	}
}
