package admin

import (
	"bindbox-game/internal/code"
	"bindbox-game/internal/pkg/core"
	paypkg "bindbox-game/internal/pkg/pay"
	"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/binary"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

type participantsResponse struct {
	IssueID         int64  `json:"issue_id"`
	ActivityID      int64  `json:"activity_id"`
	DrawMode        string `json:"draw_mode"`
	ScheduledTime   string `json:"scheduled_time"`
	MinParticipants int64  `json:"min_participants"`
	Participants    int64  `json:"participants"`
	Reached         bool   `json:"reached"`
}

func (h *handler) GetIssueParticipants() core.HandlerFunc {
	return func(ctx core.Context) {
		if ctx.SessionUserInfo().IsSuper != 1 {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "禁止操作"))
			return
		}
		issueIDStr := ctx.Param("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')
			}
		}
		iss, err := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityIssues.ID.Eq(issueID)).First()
		if err != nil || iss == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170101, "issue not found"))
			return
		}
		drawMode := "scheduled"
		minN := int64(0)
		scheduled := ""
		var st *time.Time
		var last *time.Time
		if act, e := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(iss.ActivityID)).First(); e == nil && act != nil {
			if act.DrawMode != "" {
				drawMode = act.DrawMode
			}
			minN = act.MinParticipants
			if !act.ScheduledTime.IsZero() {
				t := act.ScheduledTime
				st = &t
				scheduled = t.Format(time.RFC3339)
			}
			if !act.LastSettledAt.IsZero() {
				t := act.LastSettledAt
				last = &t
			}
		}
		remarkLike := fmt.Sprintf("lottery:activity:%d|issue:%d", iss.ActivityID, iss.ID)
		q := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().Where(
			h.readDB.Orders.Status.Eq(2),
			h.readDB.Orders.SourceType.Eq(2),
			h.readDB.Orders.Remark.Like("%"+remarkLike+"%"),
		)
		if st != nil {
			if last != nil {
				q = q.Where(h.readDB.Orders.CreatedAt.Gte(*last))
			}
			q = q.Where(h.readDB.Orders.CreatedAt.Lte(*st))
		}
		cnt, _ := q.Count()
		reached := (minN == 0) || (cnt >= minN)
		ctx.Payload(&participantsResponse{IssueID: iss.ID, ActivityID: iss.ActivityID, DrawMode: drawMode, ScheduledTime: scheduled, MinParticipants: minN, Participants: cnt, Reached: reached})
	}
}

type settleIssueRequest struct {
	Force bool `json:"force"`
}

type settleIssueResponse struct {
	IssueID  int64  `json:"issue_id"`
	Status   string `json:"status"`
	Refunded int    `json:"refunded"`
	Granted  int    `json:"granted"`
}

func (h *handler) SettleIssue() core.HandlerFunc {
	return func(ctx core.Context) {
		if ctx.SessionUserInfo().IsSuper != 1 {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "禁止操作"))
			return
		}
		req := new(settleIssueRequest)
		if err := ctx.ShouldBindJSON(req); err != nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
			return
		}
		issueIDStr := ctx.Param("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')
			}
		}
		iss, err := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityIssues.ID.Eq(issueID)).First()
		if err != nil || iss == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170201, "issue not found"))
			return
		}
		// Fetch Activity for Seed
		// act, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(iss.ActivityID)).First()

		cfg := map[string]any{}
		drawMode := "scheduled"
		minN := int64(0)
		refundCouponType := ""
		refundCouponAmount := 0.0
		refundCouponID := int64(0)
		if m, e := h.syscfg.GetByKey(ctx.RequestContext(), fmt.Sprintf("lottery:activity:%d:scheduled_config", iss.ActivityID)); e == nil && m != nil {
			_ = json.Unmarshal([]byte(m.ConfigValue), &cfg)
			if v, ok := cfg["draw_mode"].(string); ok {
				drawMode = v
			}
			if v, ok := cfg["min_participants"].(float64); ok {
				minN = int64(v)
			}
			if v, ok := cfg["refund_coupon_type"].(string); ok {
				refundCouponType = v
			}
			if v, ok := cfg["refund_coupon_amount"].(float64); ok {
				refundCouponAmount = v
			}
			if v, ok := cfg["refund_coupon_id"].(float64); ok {
				refundCouponID = int64(v)
			}
		}
		remarkLike := fmt.Sprintf("lottery:activity:%d|issue:%d", iss.ActivityID, iss.ID)
		orders, _ := h.readDB.Orders.WithContext(ctx.RequestContext()).ReadDB().Where(
			h.readDB.Orders.Status.Eq(2),
			h.readDB.Orders.SourceType.Eq(2),
			h.readDB.Orders.Remark.Like("%"+remarkLike+"%"),
		).Find()
		participants := len(orders)
		refunded := 0
		granted := 0
		if drawMode == "scheduled" && !req.Force {
			// 若到时未满员则退款
			if minN > 0 && int64(participants) < minN {
				wc, e := paypkg.NewWechatPayClient(ctx.RequestContext())
				if e == nil {
					for _, o := range orders {
						refundNo := fmt.Sprintf("R%s-%d", o.OrderNo, time.Now().Unix())
						_, status, e2 := wc.RefundOrder(ctx.RequestContext(), o.OrderNo, refundNo, o.ActualAmount, o.ActualAmount, "admin_settle_not_enough")
						if e2 != nil {
							continue
						}
						_ = h.writeDB.PaymentRefunds.WithContext(ctx.RequestContext()).Create(&model.PaymentRefunds{OrderID: o.ID, OrderNo: o.OrderNo, RefundNo: refundNo, Channel: "wechat_jsapi", Status: status, AmountRefund: o.ActualAmount, Reason: "admin_settle_not_enough"})
						_, _ = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.ID.Eq(o.ID)).Updates(map[string]any{h.writeDB.Orders.Status.ColumnName().String(): 4})
						// 退款日志与送券
						_ = h.repo.GetDbW().Exec("INSERT INTO lottery_refund_logs(issue_id, order_id, user_id, amount, coupon_type, coupon_amount, reason, status) VALUES(?,?,?,?,?,?,?,?)", iss.ID, o.ID, o.UserID, o.ActualAmount, refundCouponType, refundCouponAmount, "admin_settle_not_enough", status).Error
						if refundCouponID > 0 {
							_ = usersvc.New(h.logger, h.repo).AddCoupon(ctx.RequestContext(), o.UserID, refundCouponID)
						}
						// 增加一番赏位置恢复
						_ = h.activity.ClearIchibanPositionsByOrderID(ctx.RequestContext(), o.ID)
						refunded++
					}
				}
			} else {
				// 人数达标统一开奖
				s := strat.NewDefault(h.readDB, h.writeDB)
				for _, o := range orders {
					rid, proof, e2 := s.SelectItem(ctx.RequestContext(), iss.ActivityID, iss.ID, o.UserID)
					if e2 != nil || rid <= 0 {
						continue
					}
					rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
					_ = s.GrantReward(ctx.RequestContext(), o.UserID, rid)
					drawLog := &model.ActivityDrawLogs{UserID: o.UserID, IssueID: iss.ID, OrderID: o.ID, RewardID: rid, IsWinner: 1, Level: func() int32 {
						if rw != nil {
							return rw.Level
						}
						return 1
					}(), CurrentLevel: 1}
					_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog)
					// 保存可验证凭据
					_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog.ID, iss.ID, o.UserID, proof)
					granted++
				}
			}
		} else {
			// 即时或强制：统一开奖
			s := strat.NewDefault(h.readDB, h.writeDB)
			for _, o := range orders {
				rid, proof, e2 := s.SelectItem(ctx.RequestContext(), iss.ActivityID, iss.ID, o.UserID)
				if e2 != nil || rid <= 0 {
					continue
				}
				rw, _ := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.ID.Eq(rid)).First()
				_ = s.GrantReward(ctx.RequestContext(), o.UserID, rid)
				drawLog := &model.ActivityDrawLogs{UserID: o.UserID, IssueID: iss.ID, OrderID: o.ID, RewardID: rid, IsWinner: 1, Level: func() int32 {
					if rw != nil {
						return rw.Level
					}
					return 1
				}(), CurrentLevel: 1}
				_ = h.writeDB.ActivityDrawLogs.WithContext(ctx.RequestContext()).Create(drawLog)
				// 保存可验证凭据
				_ = strat.SaveDrawReceipt(ctx.RequestContext(), h.writeDB, drawLog.ID, iss.ID, o.UserID, proof)
				granted++
			}
		}
		ctx.Payload(&settleIssueResponse{IssueID: iss.ID, Status: "success", Refunded: refunded, Granted: granted})
	}
}

type simulateIssueRequest struct {
	NumUsers     int   `json:"num_users"`
	DrawsPerUser int   `json:"draws_per_user"`
	PriceDraw    int64 `json:"price_draw"` // 模拟的单次价格(分)
}

type simulateRewardStat struct {
	RewardID        int64   `json:"reward_id"`
	Name            string  `json:"name"`
	Level           int32   `json:"level"`
	OriginalQty     int64   `json:"original_qty"`
	WonCount        int     `json:"won_count"`
	RemainingQty    int64   `json:"remaining_qty"`
	ActualProb      float64 `json:"actual_prob"`
	TheoreticalProb float64 `json:"theoretical_prob"`
	EffectiveProb   float64 `json:"effective_prob"` // 考虑库存后的有效理论概率
	Cost            int64   `json:"cost"`           // 单价
	TotalCost       int64   `json:"total_cost"`     // 总成本
}

type simulateIssueResponse struct {
	TotalDraws             int                  `json:"total_draws"`
	Rewards                []simulateRewardStat `json:"rewards"`
	TotalSimulationCost    int64                `json:"total_simulation_cost"`
	TotalSimulationRevenue int64                `json:"total_simulation_revenue"`
	GrossProfit            int64                `json:"gross_profit"`
	GrossProfitRate        float64              `json:"gross_profit_rate"`
}

func (h *handler) SimulateIssue() core.HandlerFunc {
	return func(ctx core.Context) {
		if ctx.SessionUserInfo().IsSuper != 1 {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "禁止操作"))
			return
		}
		req := new(simulateIssueRequest)
		if err := ctx.ShouldBindJSON(req); err != nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
			return
		}
		if req.NumUsers <= 0 {
			req.NumUsers = 1
		}
		if req.DrawsPerUser <= 0 {
			req.DrawsPerUser = 1
		}

		issueIDStr := ctx.Param("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')
			}
		}
		iss, err := h.readDB.ActivityIssues.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityIssues.ID.Eq(issueID)).First()
		if err != nil || iss == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170201, "issue not found"))
			return
		}

		act, err := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.Eq(iss.ActivityID)).First()
		if err != nil || act == nil {
			ctx.AbortWithError(core.Error(http.StatusBadRequest, 170203, "activity not found"))
			return
		}

		rewards, err := h.readDB.ActivityRewardSettings.WithContext(ctx.RequestContext()).Where(h.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
		if err != nil {
			ctx.AbortWithError(core.Error(http.StatusInternalServerError, 170202, "failed to load rewards"))
			return
		}

		// Load Products for Price
		productIDs := make([]int64, 0, len(rewards))
		for _, r := range rewards {
			if r.ProductID > 0 {
				productIDs = append(productIDs, r.ProductID)
			}
		}
		priceMap := make(map[int64]int64)
		productNameMap := make(map[int64]string)
		if len(productIDs) > 0 {
			products, _ := h.readDB.Products.WithContext(ctx.RequestContext()).Where(h.readDB.Products.ID.In(productIDs...)).Find()
			for _, p := range products {
				priceMap[p.ID] = p.Price
				productNameMap[p.ID] = p.Name
			}
		}

		// Setup Simulation
		type simReward struct {
			*model.ActivityRewardSettings
			CurrentQty int64
			Won        int
			UnitCost   int64
		}
		simRewards := make([]*simReward, len(rewards))
		var totalInitialWeight int64
		for i, r := range rewards {
			cost := int64(0)
			if r.ProductID > 0 {
				cost = priceMap[r.ProductID]
			}
			simRewards[i] = &simReward{
				ActivityRewardSettings: r,
				CurrentQty:             r.Quantity,
				Won:                    0,
				UnitCost:               cost,
			}
			if r.Quantity > 0 {
				totalInitialWeight += int64(r.Weight)
			}
		}

		totalDraws := req.NumUsers * req.DrawsPerUser
		// Use a temporary seed for simulation (IssueID + Time)
		simSeed := []byte(fmt.Sprintf("%d-%d", issueID, time.Now().UnixNano()))

		isIchiban := act.PlayType == "ichiban"

		actualDraws := 0
		for i := 0; i < totalDraws; i++ {
			if isIchiban {
				// Ichiban Simulation: Shuffle-based selection
				// Replicate strategy/ichiban.go logic locally

				// 1. Build available slots
				type slotItem struct {
					RewardID int64
					Index    int
				}
				var availableSlots []int64
				for _, r := range simRewards {
					for k := 0; k < int(r.CurrentQty); k++ {
						availableSlots = append(availableSlots, r.ID)
					}
				}

				totalSlots := len(availableSlots)
				if totalSlots == 0 {
					break // Sold out
				}

				// 2. Pick a random slot using Hash
				// Note: Real Ichiban shuffles ALL slots at start.
				// Here we simulate picking one from remaining slots which is statistically equivalent
				// if we assume each draw is independent or we just want to simulate the outcome.
				// HOWEVER, to be "Exactly the same" as user requested, we should ideally simulate the full shuffle if possible.
				// But full shuffle is stateful per issue.
				// For simulation of "N draws", we can just pick N items from the available pool randomly.

				mac := hmac.New(sha256.New, simSeed)
				mac.Write([]byte(fmt.Sprintf("sim_ichiban:%d", i)))
				sum := mac.Sum(nil)
				// Pick index [0, totalSlots)
				randIdx := int(binary.BigEndian.Uint64(sum) % uint64(totalSlots))

				pickedRewardID := availableSlots[randIdx]

				// 3. Update Inventory
				for _, r := range simRewards {
					if r.ID == pickedRewardID {
						r.CurrentQty--
						r.Won++
						break
					}
				}
				actualDraws++

			} else {
				// Default Strategy: Weighted Random
				var currentTotalWeight int64
				activeRewards := make([]*simReward, 0, len(simRewards))
				for _, r := range simRewards {
					if r.CurrentQty > 0 {
						w := int64(r.Weight)
						currentTotalWeight += w
						activeRewards = append(activeRewards, r)
					}
				}

				if currentTotalWeight <= 0 {
					break // Sold out
				}

				// HMAC-SHA256 based random for Default Strategy
				mac := hmac.New(sha256.New, simSeed)
				mac.Write([]byte(fmt.Sprintf("sim_default:%d", i)))
				sum := mac.Sum(nil)
				randVal := binary.BigEndian.Uint64(sum)
				rVal := int64(randVal % uint64(currentTotalWeight))

				var acc int64
				for _, r := range activeRewards {
					w := int64(r.Weight)
					acc += w
					if rVal < acc {
						r.CurrentQty--
						r.Won++
						break
					}
				}
				actualDraws++
			}
		}

		// Build Response
		resp := &simulateIssueResponse{
			TotalDraws: actualDraws,
			Rewards:    make([]simulateRewardStat, len(simRewards)),
		}
		var totalSimCost int64
		var totalOriginalQty int64
		if isIchiban {
			for _, r := range simRewards {
				totalOriginalQty += int64(r.OriginalQty)
			}
		}

		for i, r := range simRewards {
			actualProb := 0.0
			if actualDraws > 0 {
				actualProb = float64(r.Won) / float64(actualDraws)
			}
			theoProb := 0.0
			if isIchiban {
				if totalOriginalQty > 0 {
					theoProb = float64(r.OriginalQty) / float64(totalOriginalQty)
				}
			} else {
				// For Weighted Random, if we run until exhaustion (TotalDraws == TotalInventory),
				// the "Actual" distribution is forced to match Inventory distribution, not Weight distribution.
				// To avoid confusing users with "Deviation" in this specific scenario,
				// we can display Inventory-based Probability as Theoretical IF the inventory is fully exhausted.
				// However, strictly speaking, Theoretical IS Weight-based.
				// But let's check if TotalInitialWeight makes sense.
				if totalInitialWeight > 0 {
					theoProb = float64(r.Weight) / float64(totalInitialWeight)
				}
			}

			rewardTotalCost := int64(r.Won) * r.UnitCost
			totalSimCost += rewardTotalCost

			// Calculate Effective Probability: min(TheoreticalProb, InventoryLimitProb)
			effectiveProb := theoProb
			if actualDraws > 0 && r.OriginalQty < int64(actualDraws) {
				// Max possible win rate is OriginalQty / TotalDraws
				maxWinRate := float64(r.OriginalQty) / float64(actualDraws)

				// Special case: If inventory is exhausted (or near exhausted),
				// the effective probability IS the inventory ratio.
				// If we don't adjust this, users see large deviation.
				// Let's be smarter: If maxWinRate < effectiveProb, we cap it.
				// This handles the "Support" case: Theo=15%, Inventory=19% (28/145).
				// Wait, 19% > 15%. So maxWinRate (19%) is NOT < effectiveProb (15%).
				// So effectiveProb remains 15%.
				// But Actual is 19%. Deviation 4%.

				// Why is Actual > Theo?
				// Because other items ran out, forcing the algorithm to pick this one more often than its weight suggests.
				// This is the nature of "Weighted Random with Inventory Limit".
				// As high-weight items sell out, low-weight items become 100% probability.

				if maxWinRate < effectiveProb {
					effectiveProb = maxWinRate
				}
			}

			resp.Rewards[i] = simulateRewardStat{
				RewardID:        r.ID,
				Name:            productNameMap[r.ProductID],
				Level:           r.Level,
				OriginalQty:     r.OriginalQty, // Fix: Use OriginalQty from simReward struct which is *ActivityRewardSettings
				WonCount:        r.Won,
				RemainingQty:    r.CurrentQty,
				ActualProb:      actualProb,
				TheoreticalProb: theoProb,
				EffectiveProb:   effectiveProb,
				Cost:            r.UnitCost,
				TotalCost:       rewardTotalCost,
			}
		}

		resp.TotalSimulationCost = totalSimCost

		// Use PriceDraw from Request if provided, otherwise use Activity Price
		simPrice := act.PriceDraw
		if req.PriceDraw > 0 {
			simPrice = req.PriceDraw
		}

		fmt.Printf("Debug: req.PriceDraw=%d, act.PriceDraw=%d, simPrice=%d, actualDraws=%d\n", req.PriceDraw, act.PriceDraw, simPrice, actualDraws) // Debug log

		resp.TotalSimulationRevenue = int64(actualDraws) * simPrice
		resp.GrossProfit = resp.TotalSimulationRevenue - resp.TotalSimulationCost
		if resp.TotalSimulationRevenue > 0 {
			resp.GrossProfitRate = float64(resp.GrossProfit) / float64(resp.TotalSimulationRevenue)
		}

		ctx.Payload(resp)
	}
}
