package app

import (
	"bindbox-game/configs"
	"bindbox-game/internal/code"
	"bindbox-game/internal/pkg/core"
	"bindbox-game/internal/pkg/validation"
	"bindbox-game/internal/pkg/wechat"
	"bindbox-game/internal/repository/mysql/model"
	activitysvc "bindbox-game/internal/service/activity"
	"context"
	"crypto/rand"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"sort"
	"time"

	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
)

// ========== API Handlers ==========

type matchingGamePreOrderRequest struct {
	IssueID     int64  `json:"issue_id"`
	Position    string `json:"position"`
	CouponID    *int64 `json:"coupon_id"`
	ItemCardID  *int64 `json:"item_card_id"`
	UseGamePass bool   `json:"use_game_pass"` // 新增：是否使用次数卡
	Count       int64  `json:"count"`         // 新增：购买数量
}

type matchingGamePreOrderResponse struct {
	GameID         string `json:"game_id"`
	OrderNo        string `json:"order_no"`
	PayStatus      int32  `json:"pay_status"` // 1=Pending, 2=Paid
	ServerSeedHash string `json:"server_seed_hash"`
	// AllCards 已移除：游戏数据需通过 GetMatchingGameCards 接口在支付成功后获取
}

type matchingGameCheckRequest struct {
	GameID     string `json:"game_id" binding:"required"`
	TotalPairs int64  `json:"total_pairs"` // 客户端上报的消除总对数
}

type MatchingRewardInfo struct {
	RewardID     int64  `json:"reward_id"`
	Name         string `json:"name"`
	ProductName  string `json:"product_name"`  // 商品原始名称
	ProductImage string `json:"product_image"` // 商品图片
	Level        int32  `json:"level"`
}

type matchingGameCheckResponse struct {
	GameID     string              `json:"game_id"`
	TotalPairs int64               `json:"total_pairs"`
	Finished   bool                `json:"finished"`
	Reward     *MatchingRewardInfo `json:"reward,omitempty"`
}

// PreOrderMatchingGame 下单并预生成对对碰游戏数据
// @Summary 下单并获取对对碰全量数据
// @Description 用户下单，服务器扣费并返回全量99张乱序卡牌，前端自行负责游戏流程
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param RequestBody body matchingGamePreOrderRequest true "请求参数"
// @Success 200 {object} matchingGamePreOrderResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/matching/preorder [post]
func (h *handler) PreOrderMatchingGame() core.HandlerFunc {
	// 启动清理协程(Lazy Init)
	h.startMatchingGameCleanup()
	return func(ctx core.Context) {
		userID := int64(ctx.SessionUserInfo().Id)
		req := new(matchingGamePreOrderRequest)
		if err := ctx.ShouldBindJSON(req); err != nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
			return
		}

		// 校验 Count：对对碰只能单次购买
		if req.Count > 1 {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170013, "对对碰游戏暂不支持批量购买，请单次支付"))
			return
		}

		// 1. Get Activity/Issue Info (Mocking price for now or fetching if available)
		// Assuming price is fixed or fetched. Let's fetch basic activity info if possible, or assume config.
		// Since Request has IssueID, let's fetch Issue to get ActivityID and Price.
		// Note: The current handler doesn't have easy access to Issue struct helper without exporting or duplicating.
		// We will assume `req.IssueID` is valid and fetch price via `h.activity.GetActivity` if we had ActivityID.
		// But req only has IssueID. Let's look up Issue first.
		issue, err := h.writeDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.writeDB.ActivityIssues.ID.Eq(req.IssueID)).First()
		if err != nil || issue == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170001, "issue not found"))
			return
		}
		activity, err := h.activity.GetActivity(ctx.RequestContext(), issue.ActivityID)
		if err != nil || activity == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170001, "activity not found"))
			return
		}
		if activity.Status != 1 {
			ctx.AbortWithError(core.Error(http.StatusNotFound, 170001, "该活动已下线"))
			return
		}

		// Validation
		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
		}

		var order *model.Orders

		// ⭐ 次数卡支付分支
		if req.UseGamePass {
			// 查询用户可用的次数卡（全局或该活动的）
			now := time.Now()
			gamePasses, 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, issue.ActivityID)).
				Order(h.writeDB.UserGamePasses.ActivityID.Desc()). // 优先使用活动限定的
				Find()
			if err != nil || len(gamePasses) == 0 {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, 170012, "无可用的次数卡"))
				return
			}

			// 找到第一个未过期的次数卡
			var validPass *model.UserGamePasses
			for _, p := range gamePasses {
				if p.ExpiredAt.IsZero() || p.ExpiredAt.After(now) {
					validPass = p
					break
				}
			}
			if validPass == nil {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, 170012, "次数卡已过期"))
				return
			}

			// 扣减次数
			result, err := h.writeDB.UserGamePasses.WithContext(ctx.RequestContext()).
				Where(h.writeDB.UserGamePasses.ID.Eq(validPass.ID)).
				Where(h.writeDB.UserGamePasses.Remaining.Gt(0)).
				Updates(map[string]any{
					"remaining":  validPass.Remaining - 1,
					"total_used": validPass.TotalUsed + 1,
				})
			if err != nil || result.RowsAffected == 0 {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, 170012, "次数卡扣减失败"))
				return
			}

			// 直接创建“已支付”订单
			orderNo := now.Format("20060102150405") + fmt.Sprintf("%04d", now.UnixNano()%10000)
			newOrder := &model.Orders{
				UserID:         userID,
				OrderNo:        "GP" + orderNo,
				SourceType:     3, // 对对碰
				TotalAmount:    activity.PriceDraw,
				ActualAmount:   0, // 次数卡抵扣，实付0元
				DiscountAmount: activity.PriceDraw,
				Status:         2, // 已支付
				Remark: func() string {
					r := fmt.Sprintf("activity:%d|game_pass:%d|matching_game:issue:%d", activity.ID, validPass.ID, req.IssueID)
					if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
						r += fmt.Sprintf("|itemcard:%d", *req.ItemCardID)
					}
					return r
				}(),
				CreatedAt: now,
				UpdatedAt: now,
				PaidAt:    now,
			}

			if err := h.writeDB.Orders.WithContext(ctx.RequestContext()).
				Omit(h.writeDB.Orders.CancelledAt).
				Create(newOrder); err != nil {
				// 回滚次数卡
				h.writeDB.UserGamePasses.WithContext(ctx.RequestContext()).
					Where(h.writeDB.UserGamePasses.ID.Eq(validPass.ID)).
					Updates(map[string]any{
						"remaining":  validPass.Remaining,
						"total_used": validPass.TotalUsed,
					})
				ctx.AbortWithError(core.Error(http.StatusInternalServerError, 170002, err.Error()))
				return
			}
			order = newOrder

			// 次数卡 0 元订单手动触发任务中心
			go func() {
				_ = h.task.OnOrderPaid(context.Background(), userID, order.ID)
			}()
		} else {
			// 原有支付流程
			var couponID *int64
			if activity.AllowCoupons && req.CouponID != nil && *req.CouponID > 0 {
				couponID = req.CouponID
			}
			var itemCardID *int64
			if activity.AllowItemCards && req.ItemCardID != nil && *req.ItemCardID > 0 {
				itemCardID = req.ItemCardID
			}

			orderResult, err := h.activityOrder.CreateActivityOrder(ctx, activitysvc.CreateActivityOrderRequest{
				UserID:      userID,
				ActivityID:  issue.ActivityID,
				IssueID:     req.IssueID,
				Count:       1,
				UnitPrice:   activity.PriceDraw,
				SourceType:  3, // 对对碰
				CouponID:    couponID,
				ItemCardID:  itemCardID,
				ExtraRemark: fmt.Sprintf("matching_game:issue:%d", req.IssueID),
			})
			if err != nil {
				ctx.AbortWithError(core.Error(http.StatusInternalServerError, 170002, err.Error()))
				return
			}
			order = orderResult.Order
		}

		// 2. 加载配置
		configs, err := h.activity.ListMatchingCardTypes(ctx.RequestContext())
		if err != nil || len(configs) == 0 {
			configs = []activitysvc.CardTypeConfig{
				{Code: "A", Name: "类型A", Quantity: 9},
				{Code: "B", Name: "类型B", Quantity: 9},
				{Code: "C", Name: "类型C", Quantity: 9},
				{Code: "D", Name: "类型D", Quantity: 9},
				{Code: "E", Name: "类型E", Quantity: 9},
				{Code: "F", Name: "类型F", Quantity: 9},
				{Code: "G", Name: "类型G", Quantity: 9},
				{Code: "H", Name: "类型H", Quantity: 9},
				{Code: "I", Name: "类型I", Quantity: 9},
				{Code: "J", Name: "类型J", Quantity: 9},
				{Code: "K", Name: "类型K", Quantity: 9},
			}
		}

		// 3. 创建游戏并洗牌
		// 使用 Activity Commitment 作为随机源（必须存在）
		if len(activity.CommitmentSeedMaster) == 0 {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170011, "活动尚未生成承诺，无法开始游戏"))
			return
		}

		// 🔍 【关键修复】对配置进行强制排序，保证洗牌前的初始数组顺序绝对固定
		sort.Slice(configs, func(i, j int) bool {
			return configs[i].Code < configs[j].Code
		})

		game := activitysvc.NewMatchingGameWithConfig(configs, req.Position, activity.CommitmentSeedMaster)
		if game == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170011, "活动尚未生成承诺，无法开始游戏"))
			return
		}
		game.ActivityID = issue.ActivityID
		game.IssueID = req.IssueID
		game.OrderID = order.ID
		game.UserID = userID
		game.Position = req.Position // 保存用户选择的类型，用于服务端验证
		game.CreatedAt = time.Now()  // 设置游戏创建时间，用于自动开奖超时判断

		// 4. 构造 AllCards (仅需返回 Flat List)
		// game.deck 包含了所有的牌（已洗好，且包含了 board[0..8] 因为 NewMatchingGameWithConfig 中我们是从 deck 发到 board 的）
		// 但 NewMatchingGameWithConfig 目前的逻辑是：生成 -> 洗牌 -> 发前9张到 board -> deck只剩剩下的。
		// 所以我们需要把 board 和 deck 拼起来。
		allCards := make([]activitysvc.MatchingCard, 0, 99)
		for _, c := range game.Board {
			if c != nil {
				allCards = append(allCards, *c)
			}
		}
		for _, c := range game.Deck {
			allCards = append(allCards, *c)
		}

		// 5. 生成GameID并存储 (主要用于 Check 时校验存在性，或者验签)
		gameID := fmt.Sprintf("MG%d%d", userID, time.Now().UnixNano())

		// Save to Redis
		if err := h.activity.SaveMatchingGameToRedis(ctx.RequestContext(), gameID, game); err != nil {
			h.logger.Error("Failed to save matching game session", zap.Error(err))
			ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "failed to create game session"))
			return
		}

		// 6. Save Verification Data (ActivityDrawLogs + ActivityDrawReceipts)
		// This is required for the "Verification" feature in App/Admin to work.
		// A "Matching Game" session is treated as one "Draw".

		// 6.1 Create DrawLog
		drawLog := &model.ActivityDrawLogs{
			UserID:    userID,
			IssueID:   req.IssueID,
			OrderID:   order.ID,
			CreatedAt: time.Now(),
			IsWinner:  0, // Will be updated if they win prizes at `Check`? Or just 0 for participation.
			Level:     0,
		}
		_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog)

		// 6.2 Create DrawReceipt
		if drawLog.ID > 0 {
			receipt := &model.ActivityDrawReceipts{
				CreatedAt:      time.Now(),
				DrawLogID:      drawLog.ID,
				AlgoVersion:    "HMAC-SHA256-v1",
				RoundID:        req.IssueID,
				DrawID:         time.Now().UnixNano(), // Use timestamp to ensure uniqueness as we don't have real DrawID
				ClientID:       userID,
				Timestamp:      time.Now().UnixMilli(),
				ServerSeedHash: game.ServerSeedHash,
				ServerSubSeed:  "",           // Matching game generic seed
				ClientSeed:     req.Position, // Use Position as ClientSeed
				Nonce:          0,
				ItemsRoot:      "", // Could enable if we hashed the deck
				WeightsTotal:   0,
				SelectedIndex:  0,
				RandProof:      "",
				Signature:      "",
			}
			// Hex encode server seed
			receipt.ServerSubSeed = hex.EncodeToString(game.ServerSeed)

			_ = h.writeDB.ActivityDrawReceipts.WithContext(ctx.RequestContext()).Create(receipt)
		}

		// 7. 返回数据（不返回 all_cards，需支付成功后通过 GetMatchingGameCards 获取）
		rsp := &matchingGamePreOrderResponse{
			GameID:         gameID,
			OrderNo:        order.OrderNo,
			PayStatus:      order.Status,
			ServerSeedHash: game.ServerSeedHash,
		}

		ctx.Payload(rsp)
	}
}

// CheckMatchingGame 游戏结束结算校验
// @Summary 游戏结束结算校验
// @Description 前端游戏结束后上报结果，服务器发放奖励
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param RequestBody body matchingGameCheckRequest true "请求参数"
// @Success 200 {object} matchingGameCheckResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/matching/check [post]
func (h *handler) CheckMatchingGame() core.HandlerFunc {
	return func(ctx core.Context) {
		req := new(matchingGameCheckRequest)
		if err := ctx.ShouldBindJSON(req); err != nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
			return
		}

		// 1. Concurrency Lock: Prevent multiple check requests for the same game
		lockKey := fmt.Sprintf("lock:matching_game:check:%s", req.GameID)
		locked, err := h.redis.SetNX(ctx.RequestContext(), lockKey, "1", 10*time.Second).Result()
		if err != nil {
			ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "redis error"))
			return
		}
		if !locked {
			ctx.AbortWithError(core.Error(http.StatusConflict, 170005, "结算处理中，请勿重复提交"))
			return
		}
		defer h.redis.Del(ctx.RequestContext(), lockKey)

		game, err := h.activity.GetMatchingGameFromRedis(ctx.RequestContext(), req.GameID)
		if err != nil {
			if err == redis.Nil {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "game session not found or expired"))
			} else {
				h.logger.Error("Failed to load matching game session", zap.Error(err))
				ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "internal server error"))
			}
			return
		}

		// 校验：不能超过理论最大对数
		// 【关键校验】检查订单是否已支付
		// 对对碰游戏必须先支付才能结算和发奖
		order, err := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(game.OrderID)).First()
		if err != nil || order == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170003, "订单不存在"))
			return
		}
		if order.Status != 2 {
			h.logger.Debug("对对碰Check: 订单支付确认中",
				zap.Int64("order_id", order.ID),
				zap.Int32("status", order.Status))
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170004, "支付确认中，请稍后重试"))
			return
		}

		// 检查活动状态
		activity, err := h.activity.GetActivity(ctx.RequestContext(), game.ActivityID)
		if err != nil || activity == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170001, "活动不存在"))
			return
		}
		if activity.Status != 1 {
			ctx.AbortWithError(core.Error(http.StatusNotFound, 170001, "该活动已下线"))
			return
		}

		// 【核心安全校验】使用服务端模拟计算实际对数，不信任客户端提交的值
		serverSimulatedPairs := game.SimulateMaxPairs()
		h.logger.Debug("对对碰Check: 服务端模拟验证",
			zap.Int64("client_pairs", req.TotalPairs),
			zap.Int64("server_simulated", serverSimulatedPairs),
			zap.Int64("max_possible", game.MaxPossiblePairs),
			zap.String("position", game.Position),
			zap.String("game_id", req.GameID))

		// 使用服务端模拟的对数，而非客户端提交的值
		// 这样即使客户端伪造数据也无法作弊
		actualPairs := serverSimulatedPairs

		// 如果客户端提交的值与服务端模拟不一致，记录警告日志（可能是作弊尝试）
		if req.TotalPairs != serverSimulatedPairs {
			h.logger.Warn("对对碰Check: 客户端提交数值与服务端模拟不一致",
				zap.Int64("client_pairs", req.TotalPairs),
				zap.Int64("server_simulated", serverSimulatedPairs),
				zap.String("game_id", req.GameID))
		}

		game.TotalPairs = actualPairs // 使用服务端验证后的值
		var rewardInfo *MatchingRewardInfo

		// 【幂等性检查】在发奖前检查该订单是否已经获得过奖励
		existingLog, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(
			h.readDB.ActivityDrawLogs.OrderID.Eq(game.OrderID),
			h.readDB.ActivityDrawLogs.IsWinner.Eq(1),
		).First()
		if existingLog != nil {
			h.logger.Warn("对对碰Check: 订单已获得过奖励，拒绝重复发放",
				zap.Int64("order_id", game.OrderID),
				zap.Int64("existing_log_id", existingLog.ID))
			// 返回已有的奖励信息而不是重复发放
			if existingLog.RewardID > 0 {
				rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(
					h.readDB.ActivityRewardSettings.ID.Eq(existingLog.RewardID)).First()
				if rw != nil {
					prodName := ""
					prodImage := ""
					if p, _ := h.readDB.Products.WithContext(ctx.RequestContext()).Where(h.readDB.Products.ID.Eq(rw.ProductID)).First(); p != nil {
						prodName = p.Name
						prodImage = getFirstImage(p.ImagesJSON)
					}
					rewardInfo = &MatchingRewardInfo{
						RewardID:     rw.ID,
						Name:         prodName,
						ProductName:  prodName,
						ProductImage: prodImage,
						Level:        rw.Level,
					}
				}
			}
			ctx.Payload(&matchingGameCheckResponse{
				GameID:     req.GameID,
				TotalPairs: req.TotalPairs,
				Finished:   true,
				Reward:     rewardInfo,
			})
			return
		}

		// 1. Fetch Rewards
		rewards, err := h.activity.ListIssueRewards(ctx.RequestContext(), game.IssueID)

		if err == nil && len(rewards) > 0 {
			// 2. Filter & Sort
			var candidate *model.ActivityRewardSettings
			for _, r := range rewards {
				if r.Quantity <= 0 {
					continue
				}
				// 精确匹配：服务端验证的对子数 == 奖品设置的对子数
				if actualPairs == r.MinScore {
					candidate = r
					break // 找到精确匹配，直接使用
				}
			}

			if candidate != nil {
				// 3. Prepare Grant Params
				// Fetch real product name for remark
				productName := ""
				if p, _ := h.readDB.Products.WithContext(ctx.RequestContext()).Where(h.readDB.Products.ID.Eq(candidate.ProductID)).First(); p != nil {
					productName = p.Name
				}

				finalReward := candidate
				finalQuantity := 1
				finalRemark := fmt.Sprintf("%s %s", order.OrderNo, productName)
				var cardToVoid int64 = 0

				// 4. Apply Item Card Effects (Determine final reward and quantity)
				icID := parseItemCardIDFromRemark(order.Remark)
				h.logger.Debug("CheckMatchingGame: 道具卡检查",
					zap.String("order_no", order.OrderNo),
					zap.String("remark", order.Remark),
					zap.Int64("icID", icID))

				if icID > 0 {
					uic, _ := h.readDB.UserItemCards.WithContext(ctx.RequestContext()).Where(
						h.readDB.UserItemCards.ID.Eq(icID),
						h.readDB.UserItemCards.UserID.Eq(game.UserID),
					).First()
					if uic == nil {
						h.logger.Warn("CheckMatchingGame: 用户道具卡未找到", zap.Int64("icID", icID), zap.Int64("user_id", game.UserID))
					} else if uic.Status != 1 {
						h.logger.Warn("CheckMatchingGame: 用户道具卡状态无效", zap.Int32("status", uic.Status))
					} else { // Status == 1
						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 == game.ActivityID) || (ic.ScopeType == 4 && ic.IssueID == game.IssueID)
							h.logger.Debug("道具卡-CheckMatchingGame: 范围检查",
								zap.Int32("scope_type", ic.ScopeType),
								zap.Int64("activity_id", game.ActivityID),
								zap.Int64("issue_id", game.IssueID),
								zap.Bool("is_ok", scopeOK))

							if scopeOK {
								// Fix: Don't set cardToVoid immediately. Only set it if an effect is actually applied.

								// Double reward
								if ic.EffectType == 1 && ic.RewardMultiplierX1000 >= 2000 {
									cardToVoid = icID // Mark for consumption
									h.logger.Info("道具卡-CheckMatchingGame: 应用双倍奖励", zap.Int32("multiplier", ic.RewardMultiplierX1000))
									finalQuantity = 2
									finalRemark += "(倍数)"
								} else if ic.EffectType == 2 && ic.BoostRateX1000 > 0 {
									// Probability boost
									cardToVoid = icID // Mark for consumption (even if RNG fails, the card is "used")

									h.logger.Debug("道具卡-CheckMatchingGame: 应用概率提升", zap.Int32("boost_rate", ic.BoostRateX1000))
									allRewards, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(
										h.readDB.ActivityRewardSettings.IssueID.Eq(game.IssueID),
									).Find()
									var better *model.ActivityRewardSettings
									for _, r := range allRewards {
										if r.MinScore > candidate.MinScore && r.Quantity > 0 {
											if better == nil || r.MinScore < better.MinScore {
												better = r
											}
										}
									}
									if better != nil {
										// Use crypto/rand for secure random
										randBytes := make([]byte, 4)
										rand.Read(randBytes)
										randVal := int32(binary.BigEndian.Uint32(randBytes) % 1000)
										h.logger.Debug("道具卡-CheckMatchingGame: 概率检定",
											zap.Int32("rand", randVal),
											zap.Int32("threshold", ic.BoostRateX1000))

										if randVal < ic.BoostRateX1000 {
											// 获取升级后的商品名称
											betterProdName := ""
											if better.ProductID > 0 {
												if bp, _ := h.readDB.Products.WithContext(ctx.RequestContext()).Where(h.readDB.Products.ID.Eq(better.ProductID)).First(); bp != nil {
													betterProdName = bp.Name
												}
											}
											h.logger.Info("道具卡-CheckMatchingGame: 概率提升成功",
												zap.Int64("new_reward_id", better.ID),
												zap.String("product_name", betterProdName))
											finalReward = better
											finalRemark = betterProdName + "(升级)"
										} else {
											h.logger.Debug("道具卡-CheckMatchingGame: 概率提升失败")
										}
									} else {
										h.logger.Debug("道具卡-CheckMatchingGame: 未找到更好的奖品可升级", zap.Int64("current_score", candidate.MinScore))
									}
								} else {
									// Effect not recognized or params too low
									h.logger.Warn("道具卡-CheckMatchingGame: 效果类型未知或参数无效，不消耗卡片",
										zap.Int32("effect_type", ic.EffectType),
										zap.Int32("multiplier", ic.RewardMultiplierX1000))
								}
							} else {
								h.logger.Debug("道具卡-CheckMatchingGame: 范围校验失败")
							}
						} else {
							h.logger.Debug("道具卡-CheckMatchingGame: 时间或系统卡状态无效",
								zap.Bool("has_ic", ic != nil),
								zap.Time("start", uic.ValidStart),
								zap.Time("end", uic.ValidEnd),
								zap.Time("now", now))
						}
					}
				}

				// 5. Grant Reward
				if err := h.grantRewardHelper(ctx.RequestContext(), game.UserID, game.OrderID, finalReward, finalQuantity, finalRemark); err != nil {
					h.logger.Error("Failed to grant matching reward", zap.Int64("order_id", game.OrderID), zap.Error(err))
				} else {
					prodImage := ""
					if p, _ := h.readDB.Products.WithContext(ctx.RequestContext()).Where(h.readDB.Products.ID.Eq(candidate.ProductID)).First(); p != nil {
						productName = p.Name
						prodImage = getFirstImage(p.ImagesJSON)
					}

					rewardInfo = &MatchingRewardInfo{
						RewardID:     finalReward.ID,
						Name:         productName,
						ProductName:  productName,
						ProductImage: prodImage,
						Level:        finalReward.Level,
					}

					// 6. Void Item Card (if used)
					if cardToVoid > 0 {
						h.logger.Info("道具卡-CheckMatchingGame: 核销道具卡", zap.Int64("uic_id", cardToVoid))
						now := time.Now()
						// Get DrawLog ID for the order
						drawLog, _ := h.readDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Where(
							h.readDB.ActivityDrawLogs.OrderID.Eq(game.OrderID),
						).First()
						var drawLogID int64
						if drawLog != nil {
							drawLogID = drawLog.ID
						}
						_, _ = h.writeDB.UserItemCards.WithContext(ctx.RequestContext()).Where(
							h.writeDB.UserItemCards.ID.Eq(cardToVoid),
							h.writeDB.UserItemCards.UserID.Eq(game.UserID),
							h.writeDB.UserItemCards.Status.Eq(1),
						).Updates(map[string]any{
							h.writeDB.UserItemCards.Status.ColumnName().String():         2,
							h.writeDB.UserItemCards.UsedDrawLogID.ColumnName().String():  drawLogID,
							h.writeDB.UserItemCards.UsedActivityID.ColumnName().String(): game.ActivityID,
							h.writeDB.UserItemCards.UsedIssueID.ColumnName().String():    game.IssueID,
							h.writeDB.UserItemCards.UsedAt.ColumnName().String():         now,
						})
					}
				}
			}
		}

		rsp := &matchingGameCheckResponse{
			GameID:     req.GameID,
			TotalPairs: req.TotalPairs,
			Finished:   true,
			Reward:     rewardInfo,
		}

		// 7. Virtual Shipping (Async)
		// Upload shipping info to WeChat (similar to Ichiban Kuji) so user can see "Shipped" status and reward info.
		rewardName := "无奖励"
		if rewardInfo != nil {
			rewardName = rewardInfo.Name
		}

		go func(orderID int64, orderNo string, userID int64, rName string) {
			bgCtx := context.Background()
			// 1. Get Payment Transaction
			tx, _ := h.readDB.PaymentTransactions.WithContext(bgCtx).Where(h.readDB.PaymentTransactions.OrderNo.Eq(orderNo)).First()
			if tx == nil || tx.TransactionID == "" {
				h.logger.Warn("CheckMatchingGame: No payment transaction found for shipping", zap.String("order_no", orderNo))
				return
			}

			// 2. Get User OpenID
			u, _ := h.readDB.Users.WithContext(bgCtx).Where(h.readDB.Users.ID.Eq(userID)).First()
			payerOpenid := ""
			if u != nil {
				payerOpenid = u.Openid
			}

			// 3. Construct Item Desc
			itemsDesc := fmt.Sprintf("对对碰 %s 赏品: %s", orderNo, rName)
			if len(itemsDesc) > 120 {
				itemsDesc = itemsDesc[:120]
			}

			// 4. Upload
			c := configs.Get()
			if err := wechat.UploadVirtualShippingForBackground(bgCtx, &wechat.WechatConfig{AppID: c.Wechat.AppID, AppSecret: c.Wechat.AppSecret}, tx.TransactionID, orderNo, payerOpenid, itemsDesc); err != nil {
				h.logger.Error("CheckMatchingGame: Failed to upload virtual shipping", zap.Error(err))
			} else {
				h.logger.Info("CheckMatchingGame: Virtual shipping uploaded", zap.String("order_no", orderNo), zap.String("items", itemsDesc))
			}
		}(game.OrderID, order.OrderNo, game.UserID, rewardName)

		// 结算完成，清理会话 (Delete from Redis)
		_ = h.redis.Del(ctx.RequestContext(), activitysvc.MatchingGameKeyPrefix+req.GameID)

		ctx.Payload(rsp)
	}
}

// GetMatchingGameState 获取对对碰游戏状态
// @Summary 获取对对碰游戏状态
// @Description 获取当前游戏的完整状态
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param game_id query string true "游戏ID"
// @Success 200 {object} map[string]any
// @Failure 400 {object} code.Failure
// @Router /api/app/matching/state [get]
func (h *handler) GetMatchingGameState() core.HandlerFunc {
	return func(ctx core.Context) {
		gameID := ctx.RequestInputParams().Get("game_id")
		if gameID == "" {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "game_id required"))
			return
		}

		game, err := h.activity.GetMatchingGameFromRedis(ctx.RequestContext(), gameID)
		if err != nil {
			if err == redis.Nil {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "game session not found"))
			} else {
				h.logger.Error("Failed to load matching game", zap.Error(err))
			}
			return
		}

		// Keep-Alive: Refresh Redis TTL
		h.redis.Expire(ctx.RequestContext(), activitysvc.MatchingGameKeyPrefix+gameID, 30*time.Minute)

		// 检查活动状态
		activity, err := h.activity.GetActivity(ctx.RequestContext(), game.ActivityID)
		if err != nil || activity == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170001, "活动不存在"))
			return
		}
		if activity.Status != 1 {
			ctx.AbortWithError(core.Error(http.StatusNotFound, 170001, "该活动已下线"))
			return
		}

		ctx.Payload(game.GetGameState())
	}
}

// ListMatchingCardTypes 列出对对碰卡牌类型（App端枚举）
// @Summary 列出对对碰卡牌类型
// @Description 获取所有启用的卡牌类型配置，用于App端预览或动画展示
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Success 200 {array} activitysvc.CardTypeConfig
// @Failure 400 {object} code.Failure
// @Router /api/app/matching/card_types [get]
func (h *handler) ListMatchingCardTypes() core.HandlerFunc {
	return func(ctx core.Context) {
		configs, err := h.activity.ListMatchingCardTypes(ctx.RequestContext())
		if err != nil {
			// Try to serve default configs if DB fails? Or just error safely.
			// Let's rely on DB being available.
			ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ParamBindError, err.Error()))
			return
		}
		ctx.Payload(configs)
	}
}

// matchingGameCardsResponse 游戏数据响应
type matchingGameCardsResponse struct {
	GameID   string                     `json:"game_id"`
	AllCards []activitysvc.MatchingCard `json:"all_cards"`
}

// GetMatchingGameCards 支付成功后获取游戏数据
// @Summary 获取对对碰游戏数据
// @Description 只有支付成功后才能获取游戏牌组数据，防止未支付用户获取牌组信息
// @Tags APP端.活动
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param game_id query string true "游戏ID"
// @Success 200 {object} matchingGameCardsResponse
// @Failure 400 {object} code.Failure
// @Router /api/app/matching/cards [get]
func (h *handler) GetMatchingGameCards() core.HandlerFunc {
	return func(ctx core.Context) {
		gameID := ctx.RequestInputParams().Get("game_id")
		if gameID == "" {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "game_id required"))
			return
		}

		// 1. 从 Redis 加载游戏数据
		game, err := h.activity.GetMatchingGameFromRedis(ctx.RequestContext(), gameID)
		if err != nil {
			if err == redis.Nil {
				ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "game session not found or expired"))
			} else {
				h.logger.Error("Failed to load matching game", zap.Error(err))
				ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, "internal server error"))
			}
			return
		}

		// 2. 【关键校验】检查订单是否已支付
		order, err := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.ID.Eq(game.OrderID)).First()
		if err != nil || order == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170003, "订单不存在"))
			return
		}
		if order.Status != 2 {
			h.logger.Warn("GetMatchingGameCards: 订单未支付", zap.Int64("order_id", order.ID), zap.Int32("status", order.Status))
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170004, "请先完成支付"))
			return
		}

		// 3. 检查活动状态
		activity, err := h.activity.GetActivity(ctx.RequestContext(), game.ActivityID)
		if err != nil || activity == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170001, "活动不存在"))
			return
		}
		if activity.Status != 1 {
			ctx.AbortWithError(core.Error(http.StatusNotFound, 170001, "该活动已下线"))
			return
		}

		// 4. Keep-Alive: Refresh Redis TTL
		h.redis.Expire(ctx.RequestContext(), activitysvc.MatchingGameKeyPrefix+gameID, 30*time.Minute)

		// 5. 构造并返回全量卡牌数据
		allCards := make([]activitysvc.MatchingCard, 0, 99)
		for _, c := range game.Board {
			if c != nil {
				allCards = append(allCards, *c)
			}
		}
		for _, c := range game.Deck {
			allCards = append(allCards, *c)
		}

		ctx.Payload(&matchingGameCardsResponse{
			GameID:   gameID,
			AllCards: allCards,
		})
	}
}

func getFirstImage(imagesJSON string) string {
	if imagesJSON == "" || imagesJSON == "[]" {
		return ""
	}
	// 简单解析，假设是 ["url1", "url2"] 格式
	var images []string
	if err := json.Unmarshal([]byte(imagesJSON), &images); err == nil && len(images) > 0 {
		return images[0]
	}
	return ""
}
