package selfgame

import (
	"encoding/json"
	"fmt"
	"strings"
)

const (
	PoolLevelGreen  = "green"
	PoolLevelYellow = "yellow"
	PoolLevelRed    = "red"
	PoolLevelBlack  = "black"

	StrategyCodePoolGuard         = "pool_guard"
	StrategyCodeRiskGate          = "risk_gate"
	StrategyCodeNewUserProtection = "new_user_protection"
	StrategyCodeNormalRandom      = "normal_random"

	DecisionNone           = "none"
	DecisionForceHumanWin  = "force_human_win"
	DecisionForceHumanLose = "force_human_lose"

	ForceResultHumanWin                  = "player_win"
	ForceResultHumanWinWithoutProtection = "player_win_no_protection"
	ForceResultHumanLose                 = "player_lose"

	NewUserStrategyNone        = "none"
	NewUserStrategyFirstNoLose = "first_no_lose"
	NewUserStrategySoftBoost   = "soft_boost"
	NewUserStrategyStrongBoost = "strong_boost"

	RiskTierNormal  = "normal"
	RiskTierReduced = "reduced"
	RiskTierWeak    = "weak"
	RiskTierBlocked = "blocked"

	UserStageNew = "new"
	UserStageOld = "old"

	StageOpening    = "opening"
	StageBoost      = "boost"
	StageConfidence = "confidence"
	StageCorrection = "correction"
	StageNormal     = "normal"

	DefaultNormalPhaseTargetWinRatePercent = int32(53)
)

type RandomIntFunc func(max int) (int, error)

type StakePool struct {
	AppCode            string
	GameID             string
	StakeCoin          int64
	BalanceCoin        int64
	ReservedCoin       int64
	TargetBalanceCoin  int64
	SafeBalanceCoin    int64
	WarningBalanceCoin int64
	DangerBalanceCoin  int64
	Status             string
	CreatedAtMS        int64
	UpdatedAtMS        int64
}

type StakePoolView struct {
	StakePool
	PoolLevel        string
	RequiredPoolCoin int64
	HumanWinCapacity int64
	BotMatchEnabled  bool
	BlackReason      string
}

func (pool StakePool) AvailableCoin() int64 {
	available := pool.BalanceCoin - pool.ReservedCoin
	if available < 0 {
		return 0
	}
	return available
}

func DefaultStakePool(appCode string, gameID string, stakeCoin int64, nowMS int64) StakePool {
	if stakeCoin < 0 {
		stakeCoin = 0
	}
	return StakePool{
		AppCode:            strings.TrimSpace(appCode),
		GameID:             strings.TrimSpace(gameID),
		StakeCoin:          stakeCoin,
		TargetBalanceCoin:  stakeCoin * 20,
		SafeBalanceCoin:    stakeCoin * 10,
		WarningBalanceCoin: stakeCoin * 5,
		DangerBalanceCoin:  stakeCoin * 2,
		Status:             "active",
		CreatedAtMS:        nowMS,
		UpdatedAtMS:        nowMS,
	}
}

func NormalizeStakePool(pool StakePool) StakePool {
	pool.AppCode = strings.TrimSpace(pool.AppCode)
	pool.GameID = strings.TrimSpace(pool.GameID)
	pool.Status = strings.TrimSpace(pool.Status)
	if pool.Status == "" {
		pool.Status = "active"
	}
	if pool.StakeCoin < 0 {
		pool.StakeCoin = 0
	}
	if pool.BalanceCoin < 0 {
		pool.BalanceCoin = 0
	}
	if pool.ReservedCoin < 0 {
		pool.ReservedCoin = 0
	}
	if pool.TargetBalanceCoin < 0 {
		pool.TargetBalanceCoin = 0
	}
	if pool.SafeBalanceCoin < 0 {
		pool.SafeBalanceCoin = 0
	}
	if pool.WarningBalanceCoin < 0 {
		pool.WarningBalanceCoin = 0
	}
	if pool.DangerBalanceCoin < 0 {
		pool.DangerBalanceCoin = 0
	}
	return pool
}

func PoolLevelFor(pool StakePool, requiredPoolCoin int64) string {
	pool = NormalizeStakePool(pool)
	if requiredPoolCoin < 0 {
		requiredPoolCoin = 0
	}
	if pool.Status != "" && pool.Status != "active" {
		return PoolLevelBlack
	}
	if pool.AvailableCoin() < requiredPoolCoin {
		return PoolLevelBlack
	}
	// 新表刚上线时运营可能只先导入余额，尚未配置 safe/warning/danger 阈值。
	// 这种情况下只要当前可用余额能覆盖本局最大支出，就按绿色处理，避免阈值空值把所有档位降成黄色。
	if pool.SafeBalanceCoin == 0 && pool.WarningBalanceCoin == 0 && pool.DangerBalanceCoin == 0 {
		return PoolLevelGreen
	}
	if pool.BalanceCoin <= pool.DangerBalanceCoin {
		return PoolLevelRed
	}
	if pool.BalanceCoin <= pool.WarningBalanceCoin {
		return PoolLevelYellow
	}
	if pool.BalanceCoin >= pool.SafeBalanceCoin {
		return PoolLevelGreen
	}
	return PoolLevelYellow
}

type NewUserPolicy struct {
	AppCode                         string
	GameID                          string
	Enabled                         bool
	ProtectionRounds                int32
	ProtectionHours                 int32
	MaxStakeCoin                    int64
	LifetimeSubsidyQuotaCoin        int64
	Day1SubsidyQuotaCoin            int64
	SingleRoundSubsidyCap           int64
	StrategyLevel                   string
	LoseStreakProtectionEnabled     bool
	LoseStreakTrigger               int32
	NormalPhaseTargetWinRatePercent int32
	BlackPoolRobotForceWinEnabled   bool
	CreatedAtMS                     int64
	UpdatedAtMS                     int64
}

func DefaultNewUserPolicy(appCode string, gameID string, nowMS int64) NewUserPolicy {
	return NormalizeNewUserPolicy(NewUserPolicy{
		AppCode:                         appCode,
		GameID:                          gameID,
		Enabled:                         true,
		ProtectionRounds:                30,
		ProtectionHours:                 168,
		MaxStakeCoin:                    5000,
		LifetimeSubsidyQuotaCoin:        30000,
		Day1SubsidyQuotaCoin:            15000,
		SingleRoundSubsidyCap:           5000,
		StrategyLevel:                   NewUserStrategySoftBoost,
		LoseStreakProtectionEnabled:     true,
		LoseStreakTrigger:               2,
		NormalPhaseTargetWinRatePercent: DefaultNormalPhaseTargetWinRatePercent,
		CreatedAtMS:                     nowMS,
		UpdatedAtMS:                     nowMS,
	})
}

func NormalizeNewUserPolicy(policy NewUserPolicy) NewUserPolicy {
	policy.AppCode = strings.TrimSpace(policy.AppCode)
	policy.GameID = strings.TrimSpace(policy.GameID)
	if policy.ProtectionRounds <= 0 {
		policy.ProtectionRounds = 30
	}
	if policy.ProtectionHours <= 0 {
		policy.ProtectionHours = 168
	}
	if policy.MaxStakeCoin <= 0 {
		policy.MaxStakeCoin = 5000
	}
	if policy.LifetimeSubsidyQuotaCoin <= 0 {
		policy.LifetimeSubsidyQuotaCoin = 30000
	}
	if policy.Day1SubsidyQuotaCoin <= 0 {
		policy.Day1SubsidyQuotaCoin = 15000
	}
	if policy.SingleRoundSubsidyCap <= 0 {
		policy.SingleRoundSubsidyCap = 5000
	}
	if policy.LoseStreakTrigger <= 0 {
		policy.LoseStreakTrigger = 2
	}
	if policy.NormalPhaseTargetWinRatePercent <= 0 {
		policy.NormalPhaseTargetWinRatePercent = DefaultNormalPhaseTargetWinRatePercent
	}
	if policy.NormalPhaseTargetWinRatePercent < 50 {
		policy.NormalPhaseTargetWinRatePercent = 50
	}
	if policy.NormalPhaseTargetWinRatePercent > 100 {
		policy.NormalPhaseTargetWinRatePercent = 100
	}
	switch strings.TrimSpace(policy.StrategyLevel) {
	case NewUserStrategyNone, NewUserStrategyFirstNoLose, NewUserStrategySoftBoost, NewUserStrategyStrongBoost:
	default:
		policy.StrategyLevel = NewUserStrategySoftBoost
	}
	return policy
}

type ProtectionState struct {
	AppCode             string
	GameID              string
	UserID              int64
	UsedRounds          int32
	LifetimeSubsidyCoin int64
	Day1SubsidyCoin     int64
	Status              string
	FirstAppliedAtMS    int64
	LastAppliedAtMS     int64
	CreatedAtMS         int64
	UpdatedAtMS         int64
}

func NormalizeProtectionState(state ProtectionState) ProtectionState {
	state.AppCode = strings.TrimSpace(state.AppCode)
	state.GameID = strings.TrimSpace(state.GameID)
	state.Status = strings.TrimSpace(state.Status)
	if state.Status == "" {
		state.Status = "active"
	}
	if state.UsedRounds < 0 {
		state.UsedRounds = 0
	}
	if state.LifetimeSubsidyCoin < 0 {
		state.LifetimeSubsidyCoin = 0
	}
	if state.Day1SubsidyCoin < 0 {
		state.Day1SubsidyCoin = 0
	}
	return state
}

type ProtectionEvent struct {
	AppCode                  string
	GameID                   string
	MatchID                  string
	UserID                   int64
	UsedRoundDelta           int32
	LifetimeSubsidyCoinDelta int64
	Day1SubsidyCoinDelta     int64
	CreatedAtMS              int64
}

type RiskResult struct {
	RiskScore       int32
	RiskTier        string
	BlockProtection bool
	BlockReason     string
	ReasonJSON      string
}

func NormalizeRiskResult(result RiskResult) RiskResult {
	if result.RiskScore < 0 {
		result.RiskScore = 0
	}
	if result.RiskScore > 100 {
		result.RiskScore = 100
	}
	if strings.TrimSpace(result.RiskTier) == "" {
		result.RiskTier = RiskTierForScore(result.RiskScore)
	}
	if result.RiskTier == RiskTierBlocked {
		result.BlockProtection = true
	}
	return result
}

func RiskTierForScore(score int32) string {
	switch {
	case score >= 70:
		return RiskTierBlocked
	case score >= 50:
		return RiskTierWeak
	case score >= 30:
		return RiskTierReduced
	default:
		return RiskTierNormal
	}
}

type UserStats struct {
	EffectiveBotGameCount int32
	TodayRobotGameCount   int32
	TodayNetWinCoin       int64
	SevenDayNetWinCoin    int64
	WinStreak             int32
	LoseStreak            int32
	TodayInterventions    int32
	LastInterventionAtMS  int64
}

type StrategyInput struct {
	AppCode          string
	GameID           string
	MatchID          string
	UserID           int64
	StakeCoin        int64
	NowMS            int64
	RequiredPoolCoin int64
	AllowsDraw       bool
	StakePool        StakePool
	NewUserPolicy    NewUserPolicy
	ProtectionState  ProtectionState
	RiskResult       RiskResult
	UserStats        UserStats
}

type StrategyDecision struct {
	StrategyCode            string
	Decision                string
	ReasonCode              string
	ReasonJSON              string
	ForceResult             string
	PoolLevel               string
	RequiredPoolCoin        int64
	ShouldConsumeProtection bool
	ProtectionSubsidyCoin   int64
}

type StrategyLog struct {
	AppCode         string
	LogID           string
	GameID          string
	StakeCoin       int64
	MatchID         string
	UserID          int64
	StrategyCode    string
	Decision        string
	ForceResult     string
	ReasonCode      string
	ReasonJSON      string
	PoolBalanceCoin int64
	PoolLevel       string
	CreatedAtMS     int64
}

func EvaluateRobotStrategy(input StrategyInput, randomInt RandomIntFunc) (StrategyDecision, error) {
	input.AppCode = strings.TrimSpace(input.AppCode)
	input.GameID = strings.TrimSpace(input.GameID)
	input.MatchID = strings.TrimSpace(input.MatchID)
	input.StakePool = NormalizeStakePool(input.StakePool)
	input.NewUserPolicy = NormalizeNewUserPolicy(input.NewUserPolicy)
	input.ProtectionState = NormalizeProtectionState(input.ProtectionState)
	input.RiskResult = NormalizeRiskResult(input.RiskResult)
	if input.RequiredPoolCoin < 0 {
		input.RequiredPoolCoin = 0
	}
	level := PoolLevelFor(input.StakePool, input.RequiredPoolCoin)
	base := StrategyDecision{StrategyCode: StrategyCodeNormalRandom, Decision: DecisionNone, ReasonCode: "NORMAL_RANDOM", PoolLevel: level, RequiredPoolCoin: input.RequiredPoolCoin}

	if level == PoolLevelBlack {
		if input.StakePool.Status != "" && input.StakePool.Status != "active" {
			// 档位被运营手动停用时，不论策略开关如何都不能补机器人；停用是显式运营指令，优先级高于黑色水位强制输。
			return withReason(input, base, StrategyCodePoolGuard, DecisionNone, "", "POOL_BLACK")
		}
		if input.NewUserPolicy.BlackPoolRobotForceWinEnabled {
			// 黑色水位已经无法覆盖本档位真人赢机器人时的最大净支出；开关开启后，机器人仍可补位，
			// 但只能写入“机器人获胜/真人失败”的强制结果，保证本局不会继续消耗该档位奖池。
			return withReason(input, base, StrategyCodePoolGuard, DecisionForceHumanLose, ForceResultHumanLose, "POOL_BLACK_ROBOT_FORCE_WIN")
		}
		// 默认行为保持保守：黑色水位直接熔断机器人补位，用户继续等待真人，避免并发局把档位奖池扣穿。
		return withReason(input, base, StrategyCodePoolGuard, DecisionNone, "", "POOL_BLACK")
	}
	if input.RiskResult.BlockProtection {
		// 风控只阻断运营干预，不阻断用户正常游戏；高风险用户仍能进入真人或普通机器人随机局，但不会拿到新手补贴。
		return withReason(input, base, StrategyCodeRiskGate, DecisionNone, "", "RISK_BLOCKED")
	}

	isNewCandidate := input.NewUserPolicy.Enabled &&
		input.ProtectionState.Status == "active" &&
		input.UserStats.EffectiveBotGameCount < input.NewUserPolicy.ProtectionRounds &&
		input.ProtectionState.UsedRounds < input.NewUserPolicy.ProtectionRounds
	if isNewCandidate {
		return evaluateNewUser(input, base, randomInt)
	}
	return evaluateNormalUser(input, base, randomInt)
}

func evaluateNewUser(input StrategyInput, base StrategyDecision, randomInt RandomIntFunc) (StrategyDecision, error) {
	if input.NewUserPolicy.StrategyLevel == NewUserStrategyNone {
		return withReason(input, base, StrategyCodeNewUserProtection, DecisionNone, "", "NEW_USER_POLICY_DISABLED")
	}
	if base.PoolLevel == PoolLevelRed {
		// 红色水位要求回到基础体感；这里不输出强制结果，让 dice 随机点数、rock 随机机器人手势自行结算。
		return withReason(input, base, StrategyCodePoolGuard, DecisionNone, "", "POOL_RED_NORMAL_RANDOM")
	}
	if input.StakeCoin > input.NewUserPolicy.MaxStakeCoin {
		return withReason(input, base, StrategyCodeNewUserProtection, DecisionNone, "", "NEW_USER_STAKE_TOO_HIGH")
	}
	if input.RequiredPoolCoin > input.NewUserPolicy.SingleRoundSubsidyCap {
		return withReason(input, base, StrategyCodeNewUserProtection, DecisionNone, "", "NEW_USER_SINGLE_ROUND_QUOTA_SHORT")
	}
	if input.ProtectionState.LifetimeSubsidyCoin+input.RequiredPoolCoin > input.NewUserPolicy.LifetimeSubsidyQuotaCoin {
		return withReason(input, base, StrategyCodeNewUserProtection, DecisionNone, "", "NEW_USER_LIFETIME_QUOTA_SHORT")
	}
	if input.ProtectionState.Day1SubsidyCoin+input.RequiredPoolCoin > input.NewUserPolicy.Day1SubsidyQuotaCoin {
		return withReason(input, base, StrategyCodeNewUserProtection, DecisionNone, "", "NEW_USER_DAY1_QUOTA_SHORT")
	}
	if input.NewUserPolicy.LoseStreakProtectionEnabled && input.UserStats.LoseStreak >= input.NewUserPolicy.LoseStreakTrigger {
		// 新手连输保护只在预算、档位、风控和奖池都允许之后触发；连输次数来自已结算、非平局的自研游戏，
		// 其中真人对战和机器人对战都计入，但这里只能影响下一次机器人补位局的 forced_result，不能改写真人对战结算。
		decision := base
		decision.ShouldConsumeProtection = true
		decision.ProtectionSubsidyCoin = input.RequiredPoolCoin
		return withReason(input, decision, StrategyCodeNewUserProtection, DecisionForceHumanWin, ForceResultHumanWin, "NEW_USER_LOSE_STREAK_PROTECTION")
	}

	percent := newUserPositivePercent(input)
	if percent <= 0 {
		return withReason(input, base, StrategyCodeNewUserProtection, DecisionNone, "", "NEW_USER_NEUTRAL")
	}
	if percent < 100 {
		if randomInt == nil {
			return StrategyDecision{}, fmt.Errorf("self game strategy random is not configured")
		}
		roll, err := randomInt(100)
		if err != nil {
			return StrategyDecision{}, err
		}
		if roll < 0 || roll >= 100 {
			return StrategyDecision{}, fmt.Errorf("self game strategy random roll is invalid")
		}
		if int32(roll) >= percent {
			return withReason(input, base, StrategyCodeNewUserProtection, DecisionNone, "", "NEW_USER_PROTECTION_MISS")
		}
	}

	decision := base
	decision.ShouldConsumeProtection = true
	decision.ProtectionSubsidyCoin = input.RequiredPoolCoin
	return withReason(input, decision, StrategyCodeNewUserProtection, DecisionForceHumanWin, ForceResultHumanWin, "NEW_USER_POSITIVE_FEEDBACK")
}

func evaluateNormalUser(input StrategyInput, base StrategyDecision, randomInt RandomIntFunc) (StrategyDecision, error) {
	if base.PoolLevel == PoolLevelRed {
		// 老用户阶段不再做“消耗/强制输”。红色水位只取消新手保护类运营干预，
		// 让 dice 点数和 rock 机器人手势按自然随机结算，避免把奖池保护误解成让用户必输。
		return withReason(input, base, StrategyCodePoolGuard, DecisionNone, "", "POOL_RED_NORMAL_RANDOM")
	}
	forceWinBPS := normalPhaseForceWinBPS(input.NewUserPolicy.NormalPhaseTargetWinRatePercent, input.AllowsDraw)
	if forceWinBPS <= 0 {
		// 50% 目标就是完全自然随机；这里不强制用户输，也不为了贴合低目标去反向干预。
		return withReason(input, base, StrategyCodeNormalRandom, DecisionNone, "", "NORMAL_PHASE_TARGET_BASELINE")
	}
	if randomInt == nil {
		return StrategyDecision{}, fmt.Errorf("self game normal phase random is not configured")
	}
	roll, err := randomInt(10_000)
	if err != nil {
		return StrategyDecision{}, err
	}
	if roll < 0 || roll >= 10_000 {
		return StrategyDecision{}, fmt.Errorf("self game normal phase random roll is invalid")
	}
	if roll >= forceWinBPS {
		return withReason(input, base, StrategyCodeNormalRandom, DecisionNone, "", "NORMAL_PHASE_TARGET_MISS")
	}
	decision := base
	// 成熟期目标胜率补正只服务长期体感，不属于新手补贴；用独立 forced_result 防止结算后误消耗保护额度。
	return withReason(input, decision, StrategyCodeNormalRandom, DecisionForceHumanWin, ForceResultHumanWinWithoutProtection, "NORMAL_PHASE_TARGET_HIT")
}

func normalPhaseForceWinBPS(targetPercent int32, allowsDraw bool) int {
	if targetPercent <= 50 {
		return 0
	}
	if targetPercent >= 100 {
		return 10_000
	}
	if !allowsDraw {
		// dice 自然胜率约 50%。如果目标是 T，只有 q 比例的局需要提前强制赢：
		// T = q*100% + (1-q)*50%，所以 q = 2T - 100。
		return int(targetPercent*2-100) * 100
	}
	// rock 有平局，产品口径里平局不算有效机器人局。强制赢比例 q 下：
	// 有效胜率 = (q + (1-q)/3) / (q + 2(1-q)/3)，解得 q = (2T-100)/(200-T)。
	numerator := int64(targetPercent*2-100) * 10_000
	denominator := int64(200 - targetPercent)
	return int((numerator + denominator/2) / denominator)
}

func newUserPositivePercent(input StrategyInput) int32 {
	stage := StageForEffectiveBotGameCount(input.UserStats.EffectiveBotGameCount)
	if input.NewUserPolicy.StrategyLevel == NewUserStrategyFirstNoLose && input.UserStats.EffectiveBotGameCount == 0 {
		return 100
	}

	var percent int32
	switch input.NewUserPolicy.StrategyLevel {
	case NewUserStrategyStrongBoost:
		percent = mapStagePercent(stage, 85, 75, 65, 55, 50)
	default:
		percent = mapStagePercent(stage, 70, 60, 55, 50, 50)
	}
	switch input.RiskResult.RiskTier {
	case RiskTierReduced:
		percent -= 10
	case RiskTierWeak:
		percent -= 25
	}
	if PoolLevelFor(input.StakePool, input.RequiredPoolCoin) == PoolLevelYellow {
		percent -= 10
	}
	if percent < 0 {
		return 0
	}
	if percent > 100 {
		return 100
	}
	return percent
}

func StageForEffectiveBotGameCount(count int32) string {
	switch {
	case count <= 0:
		return StageOpening
	case count <= 5:
		return StageBoost
	case count <= 15:
		return StageConfidence
	case count <= 30:
		return StageCorrection
	default:
		return StageNormal
	}
}

func mapStagePercent(stage string, opening int32, boost int32, confidence int32, correction int32, normal int32) int32 {
	switch stage {
	case StageOpening:
		return opening
	case StageBoost:
		return boost
	case StageConfidence:
		return confidence
	case StageCorrection:
		return correction
	default:
		return normal
	}
}

func withReason(input StrategyInput, decision StrategyDecision, strategyCode string, decisionCode string, forceResult string, reasonCode string) (StrategyDecision, error) {
	decision.StrategyCode = strategyCode
	decision.Decision = decisionCode
	decision.ForceResult = forceResult
	decision.ReasonCode = reasonCode
	reasonJSON, err := reasonJSON(input, decision, reasonCode)
	if err != nil {
		return StrategyDecision{}, err
	}
	decision.ReasonJSON = reasonJSON
	return decision, nil
}

func reasonJSON(input StrategyInput, decision StrategyDecision, reasonCode string) (string, error) {
	payload := map[string]any{
		"reason_code":               reasonCode,
		"user_stage":                userStage(input),
		"stage":                     StageForEffectiveBotGameCount(input.UserStats.EffectiveBotGameCount),
		"risk_score":                input.RiskResult.RiskScore,
		"risk_tier":                 input.RiskResult.RiskTier,
		"risk_block_reason":         input.RiskResult.BlockReason,
		"pool_level":                decision.PoolLevel,
		"pool_balance_coin":         input.StakePool.BalanceCoin,
		"pool_reserved_coin":        input.StakePool.ReservedCoin,
		"pool_available_coin":       input.StakePool.AvailableCoin(),
		"required_pool_coin":        input.RequiredPoolCoin,
		"effective_bot_game_count":  input.UserStats.EffectiveBotGameCount,
		"lifetime_subsidy_left":     input.NewUserPolicy.LifetimeSubsidyQuotaCoin - input.ProtectionState.LifetimeSubsidyCoin,
		"day1_subsidy_left":         input.NewUserPolicy.Day1SubsidyQuotaCoin - input.ProtectionState.Day1SubsidyCoin,
		"new_user_policy_snapshot":  input.NewUserPolicy,
		"user_stats_snapshot":       input.UserStats,
		"should_consume_protection": decision.ShouldConsumeProtection,
		"protection_subsidy_coin":   decision.ProtectionSubsidyCoin,
		"allows_draw":               input.AllowsDraw,
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	return string(raw), nil
}

func userStage(input StrategyInput) string {
	if input.UserStats.EffectiveBotGameCount < input.NewUserPolicy.ProtectionRounds && input.ProtectionState.UsedRounds < input.NewUserPolicy.ProtectionRounds {
		return UserStageNew
	}
	return UserStageOld
}
