package app

import (
	"bindbox-game/internal/code"
	"bindbox-game/internal/pkg/core"
	"bindbox-game/internal/pkg/validation"
	"bindbox-game/internal/repository/mysql/dao"
	"bindbox-game/internal/repository/mysql/model"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"math/rand"
	"net/http"
	"time"

	titlesvc "bindbox-game/internal/service/title"

	"gorm.io/gorm/clause"
)

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"`
	UseGamePass *bool   `json:"use_game_pass"`
}

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"`
	ActualAmount int64  `json:"actual_amount"`
	Status       int32  `json:"status"`
}

// 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
		}
		// DEBUG LOG: Print request params to diagnose frontend issue
		reqBytes, _ := json.Marshal(req)
		h.logger.Info(fmt.Sprintf("JoinLottery Request Params: UserID=%d Payload=%s", ctx.SessionUserInfo().Id, string(reqBytes)))

		userID := int64(ctx.SessionUserInfo().Id)
		h.logger.Info(fmt.Sprintf("JoinLottery Start: UserID=%d ActivityID=%d IssueID=%d", userID, req.ActivityID, req.IssueID))
		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
		}
		// Ichiban Restriction: No Item Cards allowed (even if Activity logic might technically allow it, enforce strict rule here)
		if activity.PlayType == "ichiban" && req.ItemCardID != nil && *req.ItemCardID > 0 {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170010, "一番赏活动不支持道具卡"))
			return
		}
		cfgMode := "scheduled"
		if activity.DrawMode != "" {
			cfgMode = activity.DrawMode
		}
		// 定时一番赏：开奖前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 {
			order.ItemCardID = *req.ItemCardID
			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
		applied := int64(0)
		// Game Pass Conflict Check: If using Game Pass, do NOT allow coupons.
		isUsingGamePass := req.UseGamePass != nil && *req.UseGamePass
		if isUsingGamePass {
			req.CouponID = nil
		}

		if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
			applied = h.applyCouponWithCap(ctx, userID, order, req.ActivityID, *req.CouponID)
			if applied > 0 {
				order.CouponID = *req.CouponID
			}
		}
		// Title Discount Logic
		// 1. Fetch active effects for this user, scoped to this activity/issue/category
		// Note: Category ID is not readily available on Activity struct in this scope easily without join, skipping detailed scope for now or fetch if needed.
		// Assuming scope by ActivityID and IssueID is enough.
		titleEffects, _ := h.title.ResolveActiveEffects(ctx.RequestContext(), userID, titlesvc.EffectScope{
			ActivityID: &req.ActivityID,
			IssueID:    &req.IssueID,
		})

		// 2. Apply Type=2 (Discount) effects
		for _, ef := range titleEffects {
			if ef.EffectType == 2 {
				// Parse ParamsJSON: {"discount_type":"percentage","value_x1000":...,"max_discount_x1000":...}
				// Simple parsing here or helper
				var p struct {
					DiscountType     string `json:"discount_type"`
					ValueX1000       int64  `json:"value_x1000"`
					MaxDiscountX1000 int64  `json:"max_discount_x1000"`
				}
				if jsonErr := json.Unmarshal([]byte(ef.ParamsJSON), &p); jsonErr == nil {
					var discount int64
					if p.DiscountType == "percentage" {
						// e.g. 900 = 90% (10% off), or value_x1000 is the discount rate?
						// Usually "value" is what you pay? Title "8折" -> value=800?
						// Let's assume value_x1000 is the DISCOUNT amount (e.g. 200 = 20% off).
						// Wait, standard is usually "multiplier". Title "Discount" usually means "Cut".
						// Let's look at `ValidateEffectParams`: "percentage" or "fixed".
						// Assume ValueX1000 is discount ratio. 200 = 20% off.
						discount = order.ActualAmount * p.ValueX1000 / 1000
					} else if p.DiscountType == "fixed" {
						discount = p.ValueX1000 // In cents
					}

					if p.MaxDiscountX1000 > 0 && discount > p.MaxDiscountX1000 {
						discount = p.MaxDiscountX1000
					}

					if discount > order.ActualAmount {
						discount = order.ActualAmount
					}

					if discount > 0 {
						order.ActualAmount -= discount
						// Append to remark or separate logging?
						if order.Remark == "" {
							order.Remark = fmt.Sprintf("title_discount:%d:%d", ef.ID, discount)
						} else {
							order.Remark += fmt.Sprintf("|title_discount:%d:%d", ef.ID, discount)
						}
					}
				}
			}
		}

		// 3. Check Game Pass (Pre-check)
		// We will do the actual deduction inside transaction, but we can fail fast here or setup variables.
		useGamePass := false
		if req.UseGamePass != nil && *req.UseGamePass {
			// Check if user has enough valid passes
			// Note: We need to find specific passes to deduct.
			// Logic: Find all valid passes, sort by activity specific first, then expire soonest?
			// Matching game logic: "ActivityID Desc" (Specific first)
			count := int(c)
			validPasses, err := h.writeDB.UserGamePasses.WithContext(ctx.RequestContext()).
				Where(h.writeDB.UserGamePasses.UserID.Eq(userID)).
				Where(h.writeDB.UserGamePasses.Remaining.Gt(0)).
				Where(h.writeDB.UserGamePasses.ActivityID.In(0, req.ActivityID)).
				Order(h.writeDB.UserGamePasses.ActivityID.Desc(), h.writeDB.UserGamePasses.ExpiredAt.Asc()). // 优先专用，然后优先过期
				Find()

			if err != nil {
				ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
				return
			}

			totalAvailable := 0
			now := time.Now()
			for _, p := range validPasses {
				if p.ExpiredAt.IsZero() || p.ExpiredAt.After(now) {
					totalAvailable += int(p.Remaining)
				}
			}

			if totalAvailable < count {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, 170012, "次数卡余额不足"))
				return
			}
			useGamePass = true
		}

		h.logger.Info(fmt.Sprintf("JoinLottery Tx Start: UserID=%d", userID))
		err = h.writeDB.Transaction(func(tx *dao.Query) error {
			// Handle Game Pass Deduction
			if useGamePass {
				count := int(c)
				validPasses, _ := tx.UserGamePasses.WithContext(ctx.RequestContext()).
					Clauses(clause.Locking{Strength: "UPDATE"}).
					Where(tx.UserGamePasses.UserID.Eq(userID)).
					Where(tx.UserGamePasses.Remaining.Gt(0)).
					Where(tx.UserGamePasses.ActivityID.In(0, req.ActivityID)).
					Order(tx.UserGamePasses.ActivityID.Desc(), tx.UserGamePasses.ExpiredAt.Asc()).
					Find()

				now := time.Now()
				deducted := 0
				for _, p := range validPasses {
					if deducted >= count {
						break
					}
					if !p.ExpiredAt.IsZero() && p.ExpiredAt.Before(now) {
						continue
					}

					canDeduct := int(p.Remaining)
					if canDeduct > (count - deducted) {
						canDeduct = count - deducted
					}

					// Update pass
					if _, err := tx.UserGamePasses.WithContext(ctx.RequestContext()).
						Where(tx.UserGamePasses.ID.Eq(p.ID)).
						Updates(map[string]any{
							"remaining":  p.Remaining - int32(canDeduct),
							"total_used": p.TotalUsed + int32(canDeduct),
						}); err != nil {
						return err
					}
					deducted += canDeduct

					// Record usage for remark (Format: gp_use:ID:Count)
					if order.Remark == "" {
						order.Remark = fmt.Sprintf("gp_use:%d:%d", p.ID, canDeduct)
					} else {
						order.Remark += fmt.Sprintf("|gp_use:%d:%d", p.ID, canDeduct)
					}
				}

				if deducted < count {
					return errors.New("次数卡余额不足")
				}

				// Set Order to be fully paid by Game Pass
				order.ActualAmount = 0
				order.SourceType = 4 // Cleanly mark as Game Pass source

				// Legacy marker for backward compatibility or simple check
				order.Remark += "|use_game_pass"
				// Note: If we change SourceType to 4, ProcessOrderLottery might skip it if checks SourceType.
				// Lottery app usually expects SourceType=2 or similar.
				// Let's KEEP SourceType as is (likely 2 for ichiban), but Amount=0 ensures it's treated as Paid.
			}

			if !useGamePass && req.UsePoints != nil && *req.UsePoints > 0 {
				bal, _ := h.user.GetPointsBalance(ctx.RequestContext(), userID)
				// req.UsePoints 是前端传入的积分数，需要转换为分
				usePtsCents, _ := h.user.PointsToCents(ctx.RequestContext(), *req.UsePoints)
				// bal 已经是分单位
				if bal > 0 && usePtsCents > bal {
					usePtsCents = bal
				}
				// deductCents 是要从订单金额中抵扣的分数
				deductCents := usePtsCents
				if deductCents > order.ActualAmount {
					deductCents = order.ActualAmount
				}

				if deductCents > 0 {
					// needPts 是实际需要扣除的分数
					needPts := deductCents
					// Inline ConsumePointsFor logic using tx
					// Lock rows
					rows, errFind := tx.UserPoints.WithContext(ctx.RequestContext()).Clauses(clause.Locking{Strength: "UPDATE"}).Where(tx.UserPoints.UserID.Eq(userID)).Order(tx.UserPoints.ValidEnd.Asc()).Find()
					if errFind != nil {
						return errFind
					}
					remain := needPts
					now := time.Now()
					var fullConsumeIDs []int64
					var lastConsumeID int64
					var lastConsumePoints int64

					for _, r := range rows {
						if !r.ValidEnd.IsZero() && r.ValidEnd.Before(now) {
							continue
						}
						if remain <= 0 {
							break
						}
						use := r.Points
						if use > remain {
							use = remain
							lastConsumeID = r.ID
							lastConsumePoints = r.Points - use
						} else {
							fullConsumeIDs = append(fullConsumeIDs, r.ID)
						}
						remain -= use
					}

					// 批量更新完全扣除的记录
					if len(fullConsumeIDs) > 0 {
						if _, errUpd := tx.UserPoints.WithContext(ctx.RequestContext()).Where(tx.UserPoints.ID.In(fullConsumeIDs...)).Updates(map[string]any{"points": 0}); errUpd != nil {
							return errUpd
						}
					}
					// 更新最后一条记录（如果有部分剩余）
					if lastConsumeID > 0 {
						if _, errUpd := tx.UserPoints.WithContext(ctx.RequestContext()).Where(tx.UserPoints.ID.Eq(lastConsumeID)).Updates(map[string]any{"points": lastConsumePoints}); errUpd != nil {
							return errUpd
						}
					}
					if remain > 0 {
						return errors.New("insufficient_points")
					}
					// Record Ledger
					led := &model.UserPointsLedger{UserID: userID, Action: "consume_order", Points: -needPts, RefTable: "orders", RefID: orderNo, Remark: "consume by lottery"}
					if errCreate := tx.UserPointsLedger.WithContext(ctx.RequestContext()).Create(led); errCreate != nil {
						return errCreate
					}

					order.PointsAmount = deductCents
					order.PointsLedgerID = led.ID
					order.ActualAmount = order.ActualAmount - deductCents
				}
			}

			// Check if fully paid (by discount, game pass, or points)
			if order.ActualAmount <= 0 {
				order.Status = 2 // Paid
				order.PaidAt = time.Now()
			}

			err = tx.Orders.WithContext(ctx.RequestContext()).Omit(tx.Orders.PaidAt, tx.Orders.CancelledAt).Create(order)
			if err != nil {
				return err
			}

			// 一番赏占位 (针对内抵扣/次数卡导致的 0 元支付成功的订单补偿占位逻辑)
			if order.Status == 2 && activity.PlayType == "ichiban" {
				for _, si := range req.SlotIndex {
					slotIdx0 := si - 1 // 转换为 0-based 索引
					// 再次检查占用情况 (事务内原子防并发)
					cnt, _ := tx.IssuePositionClaims.WithContext(ctx.RequestContext()).Where(tx.IssuePositionClaims.IssueID.Eq(req.IssueID), tx.IssuePositionClaims.SlotIndex.Eq(slotIdx0)).Count()
					if cnt > 0 {
						return errors.New("slot_unavailable")
					}
					if err := tx.IssuePositionClaims.WithContext(ctx.RequestContext()).Create(&model.IssuePositionClaims{
						IssueID:   req.IssueID,
						SlotIndex: slotIdx0,
						UserID:    userID,
						OrderID:   order.ID,
					}); err != nil {
						return err
					}
				}
			}
			// 优惠券预扣：在事务中原子性扣减余额
			// 如果余额不足（被其他并发订单消耗），事务回滚
			if applied > 0 && order.CouponID > 0 {
				// 原子更新优惠券余额和状态
				now := time.Now()
				res := tx.Orders.UnderlyingDB().Exec(`
					UPDATE user_coupons 
					SET balance_amount = balance_amount - ?,
						status = CASE WHEN balance_amount - ? <= 0 THEN 4 ELSE 4 END,
						used_order_id = ?,
						used_at = ?
					WHERE id = ? AND user_id = ? AND balance_amount >= ? AND status IN (1, 4)
				`, applied, applied, order.ID, now, order.CouponID, userID, applied)

				if res.Error != nil {
					return fmt.Errorf("优惠券预扣失败: %w", res.Error)
				}
				if res.RowsAffected == 0 {
					// 余额不足或状态不对，事务回滚
					return errors.New("优惠券余额不足或已被使用")
				}

				// 记录使用关系
				_ = tx.Orders.UnderlyingDB().Exec("INSERT INTO order_coupons (order_id, user_coupon_id, applied_amount, created_at) VALUES (?,?,?,NOW(3))", order.ID, order.CouponID, applied).Error
			}
			return nil
		})

		if err != nil {
			h.logger.Error(fmt.Sprintf("JoinLottery Tx Failed: %v", err))
			ctx.AbortWithError(core.Error(http.StatusInternalServerError, 170002, err.Error()))
			return
		}

		rsp.JoinID = joinID
		rsp.OrderNo = orderNo
		rsp.ActualAmount = order.ActualAmount
		rsp.Status = order.Status

		// 即时开奖触发（已支付 + 即时开奖模式）
		if shouldTriggerInstantDraw(order.Status, activity.DrawMode) {
			go func() {
				_ = h.activity.ProcessOrderLottery(context.Background(), order.ID)
			}()
		}
		rsp.DrawMode = 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元订单：统一由 Service 处理优惠券扣减与流水记录
			_ = h.user.DeductCouponsForPaidOrder(ctx.RequestContext(), nil, userID, order.ID, now)

			// 异步触发任务中心逻辑
			go func() {
				_ = h.task.OnOrderPaid(context.Background(), userID, order.ID)
			}()

			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"`
	Results []map[string]any `json:"results"`
	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), h.readDB.Orders.UserID.Eq(int64(ctx.SessionUserInfo().Id))).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
		}
		// 即时开奖/结果补齐：仅在订单已支付的情况下执行
		if ord != nil && ord.Status == 2 && cfgMode == "instant" {
			_ = h.activity.ProcessOrderLottery(ctx.RequestContext(), ord.ID)
		}

		// 获取最终的开奖记录
		var logs []*model.ActivityDrawLogs
		if orderID > 0 {
			logs, _ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.OrderID.Eq(orderID)).Order(h.readDB.ActivityDrawLogs.DrawIndex).Find()
		} else {
			logs, _ = h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityDrawLogs.UserID.Eq(userID), h.readDB.ActivityDrawLogs.IssueID.Eq(issueID)).Order(h.readDB.ActivityDrawLogs.DrawIndex).Find()
		}

		if len(logs) > 0 {
			// 设置第一个为主结果 (兼容旧版单个结果显示)
			lg0 := logs[0]
			rw0, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(lg0.RewardID)).First()
			// 获取第一个奖励的商品名称
			firstProdName := ""
			if rw0 != nil && rw0.ProductID > 0 {
				if p, _ := h.readDB.Products.WithContext(ctx.RequestContext()).Where(h.readDB.Products.ID.Eq(rw0.ProductID)).First(); p != nil {
					firstProdName = p.Name
				}
			}
			rsp.Result = map[string]any{
				"reward_id":   lg0.RewardID,
				"reward_name": firstProdName,
			}

			// 填充所有结果
			rewardCache := make(map[int64]*model.ActivityRewardSettings)
			productCache := make(map[int64]*model.Products)

			for _, lg := range logs {
				rw, ok := rewardCache[lg.RewardID]
				if !ok {
					rw, _ = h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(lg.RewardID)).First()
					rewardCache[lg.RewardID] = rw
				}

				var img string
				var prodName string
				if rw != nil && rw.ProductID > 0 {
					prod, ok := productCache[rw.ProductID]
					if !ok {
						prod, _ = h.readDB.Products.WithContext(ctx.RequestContext()).Where(h.readDB.Products.ID.Eq(rw.ProductID)).First()
						productCache[rw.ProductID] = prod
					}
					if prod != nil {
						prodName = prod.Name
						if prod.ImagesJSON != "" {
							var imgs []string
							if json.Unmarshal([]byte(prod.ImagesJSON), &imgs) == nil && len(imgs) > 0 {
								img = imgs[0]
							}
						}
					}
				}

				rsp.Results = append(rsp.Results, map[string]any{
					"reward_id":   lg.RewardID,
					"reward_name": prodName,
					"image":       img,
					"draw_index":  lg.DrawIndex + 1,
				})
			}
		}
		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)
	}
}

// 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")
	}
	// 1. 强制校验：必须选择位置
	if len(req.SlotIndex) == 0 {
		return core.Error(http.StatusBadRequest, code.ParamBindError, "一番赏必须选择位置")
	}
	if req.Count <= 0 || req.Count != int64(len(req.SlotIndex)) {
		return core.Error(http.StatusBadRequest, code.ParamBindError, "参数错误：数量与位置不匹配")
	}

	// 2. 内存中去重和范围检查
	selectedSlots := make([]int64, 0, len(req.SlotIndex))
	seen := make(map[int64]struct{}, len(req.SlotIndex))
	for _, si := range req.SlotIndex {
		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")
		}
		selectedSlots = append(selectedSlots, si-1)
	}

	// 3. 批量查询数据库检查格位是否已被占用
	var occupiedCount int64
	_ = h.repo.GetDbR().Raw("SELECT COUNT(*) FROM issue_position_claims WHERE issue_id=? AND slot_index IN ?", req.IssueID, selectedSlots).Scan(&occupiedCount).Error
	if occupiedCount > 0 {
		// 即使是并发场景，这里做一个 Pre-check 也能拦截大部分冲突
		return core.Error(http.StatusBadRequest, 170007, "部分位置已被占用，请刷新重试")
	}
	return nil
}
