package luckygift

import (
	"fmt"
	"strings"

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

const (
	ppmScale              int64 = 1_000_000
	highTierMultiplierPPM int64 = 10_000_000
)

// DefaultRuleConfig 返回 dynamic_v3 配置草稿。大奖累计消费门槛、充值分层和金额型风控上限都没有跨 App 通用值，
// 因而保留未配置 sentinel 且草稿默认 disabled；运营显式补齐这些值并启用后，才允许发布可运行版本。
func DefaultRuleConfig(appCode, poolID string) domain.RuleConfig {
	poolID = normalizePoolID(poolID)
	return domain.RuleConfig{
		AppCode:               appCode,
		PoolID:                poolID,
		StrategyVersion:       domain.StrategyDynamicV3,
		Enabled:               false,
		TargetRTPPPM:          980_000,
		PoolRatePPM:           980_000,
		ProfitRatePPM:         10_000,
		AnchorRatePPM:         10_000,
		SettlementWindowWager: 1_000_000,
		ControlBandPPM:        30_000,
		GiftPriceReference:    100,
		// dynamic_v3 的账本从 0 开始，任何启动资金都必须经过带管理员和原因的人工注资事实。
		InitialPoolCoins:          0,
		LossStreakGuarantee:       5,
		LowWatermarkCoins:         10_000_000,
		LowWaterNonzeroFactorPPM:  700_000,
		HighWatermarkCoins:        20_000_000,
		HighWaterNonzeroFactorPPM: 1_300_000,
		RechargeBoostWindowMS:     300_000,
		RechargeBoostFactorPPM:    1_100_000,
		// 这里只生成 rule_version=0 的未发布草稿，因此使用产品新建默认 150%/70%。已发布
		// 历史版本始终从不可变规则表读取其 0/100% sentinel，不会因本默认值被自动升级。
		User24HourRTPThresholdPPM:      1_500_000,
		User24HourOrdinaryWinFactorPPM: 700_000,
		JackpotMultiplierPPMs:          []int64{200_000_000, 500_000_000, 1_000_000_000},
		JackpotGlobalRTPMaxPPM:         980_000,
		// Deprecated aliases keep old in-process draft builders source-compatible. New API requests populate
		// the canonical round/48h fields and therefore opt into the settled-window algorithm.
		JackpotUserDayRTPMaxPPM:  960_000,
		JackpotUser72hRTPMaxPPM:  960_000,
		MaxJackpotHitsPerUserDay: 5,
		// 新草稿默认开启奖池级滚动爆奖节流；运营可按 App 风险预算调整，但不能用 0/0
		// 发布新的 dynamic_v3。历史不可变版本仍以数据库中的 0/0 sentinel 保持原语义。
		JackpotRateLimitWindowMinutes: 10,
		MaxJackpotHitsPerWindow:       3,
		// 用户阶段按累计金币流水折算的等价抽数推进；后台仍看到“抽数”，运行侧不会再被不同价格礼物误导。
		NoviceMaxEquivalentDraws: 2_000,
		NormalMaxEquivalentDraws: 20_000,
		Stages: []domain.RuleStage{
			// novice 的 0/0 只是协议和存储 sentinel；运行时未同时达到 normal 两个下限的用户都回落 novice。
			defaultRuleStage(domain.StageNovice, 0, 0, defaultSafeTierSeeds(domain.StageNovice)),
			// disabled 草稿不猜测金币门槛；normal/advanced 必须由运营按 App 口径配置后才能启用。
			defaultRuleStage(domain.StageNormal, 0, 0, defaultSafeTierSeeds(domain.StageNormal)),
			defaultRuleStage(domain.StageAdvanced, 0, 0, defaultSafeTierSeeds(domain.StageAdvanced)),
		},
	}
}

type ruleTierSeed struct {
	id            string
	multiplierPPM int64
	weightPPM     int64
}

// defaultSafeTierSeeds 保留明确的 0x 和少量 0.5x 小奖，并让每个阶段的静态期望精确为 98%。
// 200x/500x/1000x 只登记为 jackpot 倍率，不在缺少产品概率时照搬图片示例塞进基础 tier。
func defaultSafeTierSeeds(stage string) []ruleTierSeed {
	return []ruleTierSeed{
		{id: stage + "_none", multiplierPPM: 0, weightPPM: 50_000},
		{id: stage + "_0_5x", multiplierPPM: 500_000, weightPPM: 40_000},
		{id: stage + "_1x", multiplierPPM: 1_000_000, weightPPM: 860_000},
		{id: stage + "_2x", multiplierPPM: 2_000_000, weightPPM: 50_000},
	}
}

func defaultRuleStage(stage string, minRecharge7DCoins, minRecharge30DCoins int64, tiers []ruleTierSeed) domain.RuleStage {
	out := domain.RuleStage{
		Stage:               stage,
		MinRecharge7DCoins:  minRecharge7DCoins,
		MinRecharge30DCoins: minRecharge30DCoins,
		Tiers:               make([]domain.RuleTier, 0, len(tiers)),
	}
	for _, tier := range tiers {
		out.Tiers = append(out.Tiers, domain.RuleTier{
			Stage:         stage,
			TierID:        tier.id,
			MultiplierPPM: tier.multiplierPPM,
			BaseWeightPPM: tier.weightPPM,
			HighWaterOnly: tier.multiplierPPM >= highTierMultiplierPPM,
			Enabled:       true,
		})
	}
	return out
}

func normalizeRuleConfig(config domain.RuleConfig) domain.RuleConfig {
	if config.User24HourRTPThresholdPPM == 0 && config.User24HourOrdinaryWinFactorPPM == 0 {
		// proto3 标量无法区分旧客户端“未发送”和显式 0；将 0/0 收敛到唯一的禁用 sentinel，
		// 使升级前规则重发后仍保持原中奖概率，而不是意外启用或因新增字段发布失败。
		config.User24HourOrdinaryWinFactorPPM = ppmScale
	}
	// The new contract names are the only persisted/runtime meaning. During a rolling source upgrade an
	// old caller may still fill the day/72h aliases; copy them once without pretending that caller opted
	// into the settled-round algorithm. A config read from the new database columns sets the marker.
	if config.JackpotUserRoundRTPMaxPPM > 0 || config.JackpotUser48hRTPMaxPPM > 0 {
		config.SettledRoundRTPEligibility = true
	}
	if config.JackpotUserRoundRTPMaxPPM == 0 {
		config.JackpotUserRoundRTPMaxPPM = config.JackpotUserDayRTPMaxPPM
	}
	if config.JackpotUser48hRTPMaxPPM == 0 {
		config.JackpotUser48hRTPMaxPPM = config.JackpotUser72hRTPMaxPPM
	}
	if config.JackpotUserDayRTPMaxPPM == 0 {
		config.JackpotUserDayRTPMaxPPM = config.JackpotUserRoundRTPMaxPPM
	}
	if config.JackpotUser72hRTPMaxPPM == 0 {
		config.JackpotUser72hRTPMaxPPM = config.JackpotUser48hRTPMaxPPM
	}
	config.PoolID = normalizePoolID(config.PoolID)
	config.StrategyVersion = strings.ToLower(strings.TrimSpace(config.StrategyVersion))
	if config.StrategyVersion == "" {
		// 空值只可能来自升级前的客户端或旧持久行；归一为 fixed_v2 才不会让新动态校验改变历史发布语义。
		config.StrategyVersion = domain.StrategyFixedV2
	}
	for stageIndex, stage := range config.Stages {
		stage.Stage = strings.TrimSpace(stage.Stage)
		for tierIndex, tier := range stage.Tiers {
			tier.Stage = strings.TrimSpace(tier.Stage)
			if tier.Stage == "" {
				tier.Stage = stage.Stage
			}
			tier.TierID = strings.TrimSpace(tier.TierID)
			stage.Tiers[tierIndex] = tier
		}
		config.Stages[stageIndex] = stage
	}
	return config
}

func validateRuleConfig(config domain.RuleConfig) error {
	config = normalizeRuleConfig(config)
	if normalizePoolID(config.PoolID) == "" {
		return xerr.New(xerr.InvalidArgument, "lucky gift pool id is required")
	}
	if config.TargetRTPPPM < 100_000 || config.TargetRTPPPM > 990_000 {
		return xerr.New(xerr.InvalidArgument, "target RTP must be between 10% and 99%")
	}
	if config.PoolRatePPM < config.TargetRTPPPM || config.PoolRatePPM > ppmScale {
		return xerr.New(xerr.InvalidArgument, "pool rate must be between target RTP and 100%")
	}
	if config.SettlementWindowWager <= 0 {
		return xerr.New(xerr.InvalidArgument, "settlement window wager must be positive")
	}
	if config.ControlBandPPM <= 0 || config.ControlBandPPM > 50_000 {
		return xerr.New(xerr.InvalidArgument, "control band must be greater than 0 and no more than 5%")
	}
	if config.GiftPriceReference <= 0 {
		return xerr.New(xerr.InvalidArgument, "gift price reference must be positive")
	}
	if config.NoviceMaxEquivalentDraws < 0 || config.NormalMaxEquivalentDraws < config.NoviceMaxEquivalentDraws {
		return xerr.New(xerr.InvalidArgument, "equivalent draw stage thresholds are invalid")
	}
	strategyVersion := strings.ToLower(strings.TrimSpace(config.StrategyVersion))
	if strategyVersion == "" {
		strategyVersion = domain.StrategyFixedV2
	}
	if strategyVersion != domain.StrategyFixedV2 && strategyVersion != domain.StrategyDynamicV3 {
		return xerr.New(xerr.InvalidArgument, fmt.Sprintf("unsupported lucky gift strategy version: %s", config.StrategyVersion))
	}
	stageMap := make(map[string]domain.RuleStage, len(config.Stages))
	for _, stage := range config.Stages {
		if !validRuleStage(stage.Stage) {
			return xerr.New(xerr.InvalidArgument, fmt.Sprintf("invalid lucky gift stage: %s", stage.Stage))
		}
		if _, exists := stageMap[stage.Stage]; exists {
			return xerr.New(xerr.InvalidArgument, fmt.Sprintf("duplicate lucky gift stage: %s", stage.Stage))
		}
		stageMap[stage.Stage] = stage
	}
	for _, stageName := range []string{domain.StageNovice, domain.StageNormal, domain.StageAdvanced} {
		stage, exists := stageMap[stageName]
		if !exists {
			return xerr.New(xerr.InvalidArgument, "lucky gift stages must include novice, normal and advanced")
		}
		if err := validateRuleStage(stage); err != nil {
			return err
		}
		if err := validateRuleStageExpectedRTP(stage, config.TargetRTPPPM, config.ControlBandPPM); err != nil {
			return err
		}
	}
	if strategyVersion == domain.StrategyDynamicV3 {
		if config.V2HighWaterBoostThresholdCoins != 0 || config.V2HighWaterBoostFactorPPM != 0 || config.V2HighWaterBoostRecoverCoins != 0 {
			// dynamic_v3 有独立的水位因子体系；混填 v2 加权字段只会造成“看似配置了却不生效”的误解。
			return xerr.New(xerr.InvalidArgument, "v2 high-water boost fields are fixed_v2 only and must stay zero for dynamic_v3")
		}
		if err := validateDynamicRuleConfig(config, stageMap); err != nil {
			return err
		}
	}
	if strategyVersion == domain.StrategyFixedV2 {
		if err := validateV2HighWaterBoost(config); err != nil {
			return err
		}
	}
	return nil
}

// validateV2HighWaterBoost 校验 fixed_v2 高水位返奖加权三元组。
// 阈值为 0 表示关闭，此时另外两个字段必须同为 0，避免半配置发布后与运行时的关闭兜底语义不一致。
func validateV2HighWaterBoost(config domain.RuleConfig) error {
	threshold := config.V2HighWaterBoostThresholdCoins
	factor := config.V2HighWaterBoostFactorPPM
	recoverLine := config.V2HighWaterBoostRecoverCoins
	if threshold == 0 {
		if factor != 0 || recoverLine != 0 {
			return xerr.New(xerr.InvalidArgument, "disabled v2 high-water boost must keep factor and recover watermark at zero")
		}
		return nil
	}
	if threshold < 0 {
		return xerr.New(xerr.InvalidArgument, "v2 high-water boost threshold cannot be negative")
	}
	if factor <= ppmScale || factor > 10*ppmScale {
		return xerr.New(xerr.InvalidArgument, "v2 high-water boost factor must exceed 100% and stay within 1000%")
	}
	if recoverLine <= 0 || recoverLine > threshold {
		return xerr.New(xerr.InvalidArgument, "v2 high-water boost recover watermark must be positive and no higher than the threshold")
	}
	return nil
}

// validateDynamicRuleConfig 只约束 dynamic_v3；fixed_v2 的历史不可变版本即使没有这些字段也必须可以继续读取和重发。
func validateDynamicRuleConfig(config domain.RuleConfig, stages map[string]domain.RuleStage) error {
	if config.ProfitRatePPM < 0 || config.AnchorRatePPM < 0 || config.PoolRatePPM+config.ProfitRatePPM+config.AnchorRatePPM != ppmScale {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 pool, profit and anchor rates must be non-negative and sum to 100%")
	}
	if config.InitialPoolCoins != 0 {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 initial pool coins must be zero; fund the pool through an audited credit adjustment")
	}
	if config.LossStreakGuarantee <= 0 {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 loss streak guarantee must be positive")
	}
	if config.LowWatermarkCoins <= 0 || config.HighWatermarkCoins <= config.LowWatermarkCoins {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 watermarks must be positive and high watermark must exceed low watermark")
	}
	if config.LowWaterNonzeroFactorPPM <= 0 || config.LowWaterNonzeroFactorPPM >= ppmScale {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 low-water nonzero factor must be between 0% and 100%")
	}
	if config.HighWaterNonzeroFactorPPM <= ppmScale {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 high-water nonzero factor must exceed 100%")
	}
	if config.RechargeBoostWindowMS <= 0 || config.RechargeBoostFactorPPM <= ppmScale {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 recharge boost window must be positive and factor must exceed 100%")
	}
	if config.User24HourRTPThresholdPPM == 0 {
		if config.User24HourOrdinaryWinFactorPPM != ppmScale {
			return xerr.New(xerr.InvalidArgument, "dynamic_v3 disabled 24-hour RTP downweight must keep ordinary win factor at 100%")
		}
	} else if config.User24HourRTPThresholdPPM < 0 || config.User24HourOrdinaryWinFactorPPM <= 0 || config.User24HourOrdinaryWinFactorPPM > ppmScale {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 24-hour RTP threshold must be positive and ordinary win factor must be greater than 0% and no more than 100%")
	}
	if err := validateJackpotMultipliers(config.JackpotMultiplierPPMs); err != nil {
		return err
	}
	// 倍率只是返奖金额，不再承担“大奖身份”。同一倍率可以同时存在于普通概率表和补偿大奖集合；
	// 运行适配器会生成两个独立 tier，避免普通随机绕过补偿门槛或误占每日大奖次数。
	if config.JackpotGlobalRTPMaxPPM <= 0 || config.JackpotGlobalRTPMaxPPM > ppmScale ||
		config.JackpotUserRoundRTPMaxPPM <= 0 || config.JackpotUserRoundRTPMaxPPM > config.JackpotGlobalRTPMaxPPM ||
		config.JackpotUser48hRTPMaxPPM <= 0 || config.JackpotUser48hRTPMaxPPM > config.JackpotGlobalRTPMaxPPM {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 jackpot RTP limits must be positive; user limits cannot exceed the global limit")
	}
	if config.MaxJackpotHitsPerUserDay <= 0 {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 daily jackpot hit cap must be positive")
	}
	if config.JackpotRateLimitWindowMinutes <= 0 || config.JackpotRateLimitWindowMinutes > 24*60 || config.MaxJackpotHitsPerWindow <= 0 {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 jackpot rolling window must be 1-1440 minutes and its hit cap must be positive")
	}
	if config.JackpotSpendThresholdCoins < 0 {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 jackpot spend threshold coins cannot be negative")
	}
	if err := validateDynamicPayoutCaps(config, false); err != nil {
		return err
	}
	if err := validateDynamicStageThresholds(stages, config.Enabled); err != nil {
		return err
	}
	if !config.Enabled {
		// disabled 草稿允许金额口径仍为 0；启用前必须由各 App 按自身业务口径和风控预算显式补齐。
		return nil
	}
	if config.JackpotSpendThresholdCoins <= 0 {
		// Enabled V3 runs the 48h compensation and spend-milestone strategies together. A zero amount
		// would silently disable one of the two published promises, so only disabled drafts may keep 0.
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 jackpot cumulative spend threshold coins must be configured as a positive amount before enabling")
	}
	return validateDynamicPayoutCaps(config, true)
}

func validateJackpotMultipliers(multiplierPPMs []int64) error {
	if len(multiplierPPMs) == 0 {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 must configure at least one jackpot multiplier")
	}
	var previous int64
	for _, multiplierPPM := range multiplierPPMs {
		if multiplierPPM <= ppmScale || multiplierPPM <= previous {
			return xerr.New(xerr.InvalidArgument, "dynamic_v3 jackpot multipliers must exceed 1x and be strictly increasing")
		}
		previous = multiplierPPM
	}
	return nil
}

func validateDynamicStageThresholds(stages map[string]domain.RuleStage, requireConfigured bool) error {
	novice := stages[domain.StageNovice]
	normal := stages[domain.StageNormal]
	advanced := stages[domain.StageAdvanced]
	if novice.MinRecharge7DCoins != 0 || novice.MinRecharge30DCoins != 0 {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 novice recharge thresholds must stay 0/0 as protocol and storage sentinels")
	}
	if normal.MinRecharge7DCoins < 0 || normal.MinRecharge30DCoins < 0 || advanced.MinRecharge7DCoins < 0 || advanced.MinRecharge30DCoins < 0 {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 recharge thresholds cannot be negative")
	}
	thresholdsUnset := normal.MinRecharge7DCoins == 0 && normal.MinRecharge30DCoins == 0 &&
		advanced.MinRecharge7DCoins == 0 && advanced.MinRecharge30DCoins == 0
	if !requireConfigured && thresholdsUnset {
		// 未启用版本只在 normal/advanced 四个值全部为 0 时视为“纯未配置”草稿。
		// 一旦运营开始填写任一门槛，即使仍 disabled 也必须保持完整单调边界，避免保存一份无法继续发布的半成品。
		return nil
	}
	if normal.MinRecharge7DCoins == 0 && normal.MinRecharge30DCoins == 0 {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 normal stage must configure a positive 7-day or 30-day recharge threshold before enabling")
	}
	if advanced.MinRecharge7DCoins < normal.MinRecharge7DCoins || advanced.MinRecharge30DCoins < normal.MinRecharge30DCoins ||
		(advanced.MinRecharge7DCoins == normal.MinRecharge7DCoins && advanced.MinRecharge30DCoins == normal.MinRecharge30DCoins) {
		return xerr.New(xerr.InvalidArgument, "dynamic_v3 advanced recharge thresholds must be component-wise monotonic and exceed normal in at least one dimension")
	}
	return nil
}

func validateDynamicPayoutCaps(config domain.RuleConfig, requirePositive bool) error {
	caps := []struct {
		name  string
		value int64
	}{
		{name: "max_single_payout", value: config.MaxSinglePayout},
		{name: "user_hourly_payout_cap", value: config.UserHourlyPayoutCap},
		{name: "user_daily_payout_cap", value: config.UserDailyPayoutCap},
		{name: "device_daily_payout_cap", value: config.DeviceDailyPayoutCap},
		{name: "room_hourly_payout_cap", value: config.RoomHourlyPayoutCap},
		{name: "anchor_daily_payout_cap", value: config.AnchorDailyPayoutCap},
	}
	for _, cap := range caps {
		if cap.value < 0 || (requirePositive && cap.value == 0) {
			if !requirePositive {
				return xerr.New(xerr.InvalidArgument, fmt.Sprintf("dynamic_v3 %s cannot be negative", cap.name))
			}
			return xerr.New(xerr.InvalidArgument, fmt.Sprintf("dynamic_v3 %s must be positive before enabling", cap.name))
		}
	}
	return nil
}

func validateRuleStage(stage domain.RuleStage) error {
	var weightSum int64
	hasBaseTier := false
	hasZeroTier := false
	tierIDs := make(map[string]struct{}, len(stage.Tiers))
	for _, tier := range stage.Tiers {
		if tier.Stage != stage.Stage {
			return xerr.New(xerr.InvalidArgument, fmt.Sprintf("tier %s stage does not match %s", tier.TierID, stage.Stage))
		}
		if tier.TierID == "" {
			return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s tier id is required", stage.Stage))
		}
		// 同阶段奖档 ID 是 lucky_gift_stage_tiers 的主键维度；发布前拦截重复值，避免运营配置错误落到数据库唯一键后变成 opaque internal error。
		if _, exists := tierIDs[tier.TierID]; exists {
			return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s tier id is duplicated: %s", stage.Stage, tier.TierID))
		}
		tierIDs[tier.TierID] = struct{}{}
		if tier.MultiplierPPM < 0 {
			return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s tier multiplier is invalid", tier.TierID))
		}
		if tier.BaseWeightPPM < 0 {
			return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s tier probability is invalid", tier.TierID))
		}
		if tier.Enabled {
			weightSum += tier.BaseWeightPPM
			hasBaseTier = true
			hasZeroTier = hasZeroTier || tier.MultiplierPPM == 0
		}
		if tier.Enabled && tier.MultiplierPPM >= highTierMultiplierPPM && !tier.HighWaterOnly {
			return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s high multiplier tier must be high-water only", tier.TierID))
		}
	}
	if weightSum != ppmScale {
		return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s stage probability must sum to 100%%", stage.Stage))
	}
	if !hasBaseTier {
		return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s stage must include at least one base RTP tier", stage.Stage))
	}
	if !hasZeroTier {
		return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s stage must include explicit 0x tier", stage.Stage))
	}
	return nil
}

func validateRuleStageExpectedRTP(stage domain.RuleStage, targetRTPPPM int64, controlBandPPM int64) error {
	var expectedPPM int64
	for _, tier := range stage.Tiers {
		if !tier.Enabled {
			continue
		}
		expectedPPM += tier.MultiplierPPM * tier.BaseWeightPPM / ppmScale
	}
	lower := targetRTPPPM - controlBandPPM
	upper := targetRTPPPM + controlBandPPM
	if expectedPPM < lower || expectedPPM > upper {
		return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s stage expected RTP %.2f%% must be within target band %.2f%%-%.2f%%",
			stage.Stage,
			float64(expectedPPM)/10_000,
			float64(lower)/10_000,
			float64(upper)/10_000,
		))
	}
	return nil
}

func validRuleStage(stage string) bool {
	return stage == domain.StageNovice || stage == domain.StageNormal || stage == domain.StageAdvanced
}

func normalizePoolID(poolID string) string {
	poolID = strings.TrimSpace(poolID)
	if poolID == "" {
		return domain.DefaultPoolID
	}
	return poolID
}
