package mysql

import (
	crand "crypto/rand"
	"fmt"
	"math/big"
	"sort"
	"time"

	domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)

type luckyCryptoStrategyRandom struct{}

func (luckyCryptoStrategyRandom) Int63n(n int64) (int64, error) {
	if n <= 0 {
		return 0, fmt.Errorf("lucky strategy random bound must be positive")
	}
	value, err := crand.Int(crand.Reader, big.NewInt(n))
	if err != nil {
		return 0, err
	}
	return value.Int64(), nil
}

func luckyUsesDynamicStrategy(config domain.Config) bool {
	return config.StrategyVersion == domain.StrategyDynamicV3
}

// BuildLuckyDynamicStrategyForRule exposes the same read-only RuleConfig -> runtime ->
// pure-kernel adapter used by production. Offline simulators must enter through this
// boundary instead of hand-assembling StrategyTier values, otherwise a report can pass
// while silently omitting stage merging, generated source IDs or a newly added rule field.
// The function performs no database I/O and does not enable or publish the supplied rule.
func BuildLuckyDynamicStrategyForRule(rule domain.RuleConfig) (domain.Config, domain.StrategyConfig, error) {
	config, err := luckyRuntimeConfigFromRuleConfig(rule)
	if err != nil {
		return domain.Config{}, domain.StrategyConfig{}, err
	}
	return config, luckyDynamicStrategyConfig(config), nil
}

// luckyDynamicStrategyConfig 把不可变规则快照转换为纯策略输入。
// 同一倍率在三个充值阶段可能使用不同 tier_id；普通路径按倍率合并并保存 stage->weight，
// 大奖路径则始终生成独立 jackpot tier。倍率只决定金额，ordinary/jackpot tier ID 才决定
// 命中来源，这样普通 200x 与补偿 200x 可以共存且不会共享每日大奖计数语义。
func luckyDynamicStrategyConfig(config domain.Config) domain.StrategyConfig {
	stageNames := make([]string, 0, len(config.Stages))
	rechargeStages := make([]domain.StrategyRechargeStage, 0, len(config.Stages))
	for _, stage := range config.Stages {
		stageNames = append(stageNames, stage.Stage)
		rechargeStages = append(rechargeStages, domain.StrategyRechargeStage{
			Name:                stage.Stage,
			MinRecharge7DCoins:  stage.MinRecharge7DCoins,
			MinRecharge30DCoins: stage.MinRecharge30DCoins,
		})
	}
	jackpotSet := make(map[int64]struct{}, len(config.JackpotMultiplierPPMs))
	for _, multiplier := range config.JackpotMultiplierPPMs {
		jackpotSet[multiplier] = struct{}{}
	}
	ordinaryByMultiplier := make(map[int64]*domain.StrategyTier)
	for _, stage := range config.Stages {
		for _, configured := range stage.Tiers {
			if !configured.Enabled {
				continue
			}
			tier := ordinaryByMultiplier[configured.MultiplierPPM]
			if tier == nil {
				stageWeights := make(map[string]int64, len(stageNames))
				for _, stageName := range stageNames {
					stageWeights[stageName] = 0
				}
				tier = &domain.StrategyTier{
					// 保留既有普通奖档审计 ID；来源隔离由普通 multiplier_ 与大奖 jackpot_ 前缀完成。
					ID:             fmt.Sprintf("multiplier_%d", configured.MultiplierPPM),
					MultiplierPPM:  configured.MultiplierPPM,
					StageWeightPPM: stageWeights,
					Enabled:        true,
				}
				ordinaryByMultiplier[configured.MultiplierPPM] = tier
			}
			tier.StageWeightPPM[stage.Stage] += configured.BaseWeightPPM
		}
	}
	// 大奖集合即使与普通倍率重合也另建 tier；其普通阶段权重固定为 0，只能在 RTP
	// 补偿条件通过后由 selectPayableJackpot 选中。当前协议没有大奖内部权重，统一记 1。
	jackpotByMultiplier := make(map[int64]domain.StrategyTier, len(jackpotSet))
	for multiplier := range jackpotSet {
		stageWeights := make(map[string]int64, len(stageNames))
		for _, stageName := range stageNames {
			stageWeights[stageName] = 0
		}
		jackpotByMultiplier[multiplier] = domain.StrategyTier{
			ID:             fmt.Sprintf("jackpot_%d", multiplier),
			MultiplierPPM:  multiplier,
			StageWeightPPM: stageWeights,
			Jackpot:        true,
			JackpotWeight:  1,
			Enabled:        true,
		}
	}
	multipliers := make([]int64, 0, len(ordinaryByMultiplier))
	for multiplier := range ordinaryByMultiplier {
		multipliers = append(multipliers, multiplier)
	}
	sort.Slice(multipliers, func(i, j int) bool { return multipliers[i] < multipliers[j] })
	jackpotMultipliers := make([]int64, 0, len(jackpotByMultiplier))
	for multiplier := range jackpotByMultiplier {
		jackpotMultipliers = append(jackpotMultipliers, multiplier)
	}
	sort.Slice(jackpotMultipliers, func(i, j int) bool { return jackpotMultipliers[i] < jackpotMultipliers[j] })
	tiers := make([]domain.StrategyTier, 0, len(multipliers)+len(jackpotMultipliers))
	for _, multiplier := range multipliers {
		tiers = append(tiers, *ordinaryByMultiplier[multiplier])
	}
	for _, multiplier := range jackpotMultipliers {
		tiers = append(tiers, jackpotByMultiplier[multiplier])
	}
	return domain.StrategyConfig{
		RuleVersion:                    config.RuleVersion,
		Tiers:                          tiers,
		PublicPoolRatePPM:              config.PoolRatePPM,
		ProfitPoolRatePPM:              config.ProfitRatePPM,
		AnchorReturnRatePPM:            config.AnchorRatePPM,
		ColdStartPoolCoins:             config.InitialPoolCoins,
		LowWaterThresholdCoins:         config.LowWatermarkCoins,
		HighWaterThresholdCoins:        config.HighWatermarkCoins,
		LowWaterFactorPPM:              config.LowWaterNonzeroFactorPPM,
		HighWaterFactorPPM:             config.HighWaterNonzeroFactorPPM,
		RechargeFactorPPM:              config.RechargeBoostFactorPPM,
		RechargeBoostStartMS:           0,
		RechargeBoostEndMS:             config.RechargeBoostWindowMS,
		User24HourRTPThresholdPPM:      config.User24HourRTPThresholdPPM,
		User24HourOrdinaryWinFactorPPM: config.User24HourOrdinaryWinFactorPPM,
		MissProtectionZeroDraws:        config.LossStreakGuarantee,
		DailyJackpotLimit:              config.MaxJackpotHitsPerUserDay,
		MilestoneSpendCoins:            config.JackpotSpendThresholdCoins,
		JackpotRateLimitWindowMS:       config.JackpotRateLimitWindowMinutes * int64(time.Minute/time.Millisecond),
		JackpotRateLimit:               config.MaxJackpotHitsPerWindow,
		JackpotMechanism1Enabled:       true,
		JackpotMechanism2Enabled:       config.JackpotSpendThresholdCoins > 0,
		GlobalRTPMaxPPM:                config.JackpotGlobalRTPMaxPPM,
		UserRoundRTPMaxPPM:             config.JackpotUserRoundRTPMaxPPM,
		User48HourRTPMaxPPM:            config.JackpotUser48hRTPMaxPPM,
		SettledRoundRTPEligibility:     true,
		RechargeStages:                 rechargeStages,
	}
}

// luckyDynamicFundSplit 以 wallet 已按后台礼物比例配置实际入账的主播钻石为唯一主播份额。
// 幸运礼物规则只决定公共奖池比例；主播份额变化后的剩余金额自然进入平台利润，不能再用规则里的
// anchor_rate_ppm 覆盖 wallet owner 事实，也不能把“返还金币比例”误当成主播收益。
func luckyDynamicFundSplit(totalSpent, actualAnchorIncome int64, config domain.StrategyConfig) (domain.StrategyFundSplit, error) {
	configured, err := domain.SplitLuckyGiftStrategyFunds(totalSpent, config)
	if err != nil {
		return domain.StrategyFundSplit{}, err
	}
	if actualAnchorIncome < 0 || actualAnchorIncome > totalSpent || configured.PublicPoolCoins > totalSpent-actualAnchorIncome {
		return domain.StrategyFundSplit{}, fmt.Errorf("dynamic lucky gift wallet anchor income is invalid: spent=%d anchor=%d", totalSpent, actualAnchorIncome)
	}
	return domain.StrategyFundSplit{
		PublicPoolCoins:   configured.PublicPoolCoins,
		ProfitPoolCoins:   totalSpent - configured.PublicPoolCoins - actualAnchorIncome,
		AnchorReturnCoins: actualAnchorIncome,
	}, nil
}

// luckyDynamicUnitFundSplits 联合分配整批的三个资金桶：每一抽都先满足
// public_i+profit_i+anchor_i=unit_spent_i，同时整批三桶又精确等于总拆账。
// 不能分别对四个总量调用“余数落前几抽”，否则 100 金币/99 抽的首抽会出现 spent=2、三桶=3。
func luckyDynamicUnitFundSplits(totalSpent int64, drawCount int32, total domain.StrategyFundSplit) ([]domain.StrategyFundSplit, error) {
	if totalSpent <= 0 || drawCount <= 0 || total.PublicPoolCoins < 0 || total.ProfitPoolCoins < 0 || total.AnchorReturnCoins < 0 ||
		total.PublicPoolCoins+total.ProfitPoolCoins+total.AnchorReturnCoins != totalSpent {
		return nil, fmt.Errorf("dynamic lucky gift batch split is invalid")
	}
	units := make([]int64, drawCount)
	for index := int32(1); index <= drawCount; index++ {
		units[index-1] = luckyDrawUnitSpend(totalSpent, drawCount, index)
		if units[index-1] <= 0 {
			return nil, fmt.Errorf("dynamic lucky gift unit spend must be positive")
		}
	}
	publicParts, err := luckyDynamicApportionByCapacity(units, total.PublicPoolCoins)
	if err != nil {
		return nil, err
	}
	remaining := make([]int64, len(units))
	for index := range units {
		remaining[index] = units[index] - publicParts[index]
	}
	anchorParts, err := luckyDynamicApportionByCapacity(remaining, total.AnchorReturnCoins)
	if err != nil {
		return nil, err
	}
	parts := make([]domain.StrategyFundSplit, len(units))
	var publicSum, profitSum, anchorSum int64
	for index := range units {
		parts[index] = domain.StrategyFundSplit{
			PublicPoolCoins:   publicParts[index],
			AnchorReturnCoins: anchorParts[index],
			ProfitPoolCoins:   units[index] - publicParts[index] - anchorParts[index],
		}
		publicSum += parts[index].PublicPoolCoins
		profitSum += parts[index].ProfitPoolCoins
		anchorSum += parts[index].AnchorReturnCoins
	}
	if publicSum != total.PublicPoolCoins || profitSum != total.ProfitPoolCoins || anchorSum != total.AnchorReturnCoins {
		return nil, fmt.Errorf("dynamic lucky gift unit split does not conserve bucket totals")
	}
	return parts, nil
}

// luckyDynamicApportionByCapacity 按每抽金额占比做累计配额，既避免前置抽被贪心塞满，
// 又保证每份不超过该抽剩余容量。big.Int 只用于防止 total*part 在大额外部请求上溢出 int64。
func luckyDynamicApportionByCapacity(capacities []int64, partTotal int64) ([]int64, error) {
	var capacityTotal int64
	for _, capacity := range capacities {
		if capacity < 0 || capacityTotal > int64(^uint64(0)>>1)-capacity {
			return nil, fmt.Errorf("dynamic lucky gift unit capacity is invalid")
		}
		capacityTotal += capacity
	}
	if partTotal < 0 || partTotal > capacityTotal || capacityTotal <= 0 {
		return nil, fmt.Errorf("dynamic lucky gift apportioned total exceeds capacity")
	}
	parts := make([]int64, len(capacities))
	denominator := big.NewInt(capacityTotal)
	var prefix, allocated int64
	for index, capacity := range capacities {
		prefix += capacity
		cumulative := new(big.Int).Mul(big.NewInt(prefix), big.NewInt(partTotal))
		cumulative.Quo(cumulative, denominator)
		if !cumulative.IsInt64() {
			return nil, fmt.Errorf("dynamic lucky gift unit apportion overflow")
		}
		parts[index] = cumulative.Int64() - allocated
		if parts[index] < 0 || parts[index] > capacity {
			return nil, fmt.Errorf("dynamic lucky gift unit apportion exceeded one draw")
		}
		allocated += parts[index]
	}
	if allocated != partTotal {
		return nil, fmt.Errorf("dynamic lucky gift unit apportion lost funds")
	}
	return parts, nil
}
