package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"strings"

	"hyapp/pkg/appcode"
	dicedomain "hyapp/services/game-service/internal/domain/dice"
	selfgame "hyapp/services/game-service/internal/domain/selfgame"
)

func (r *Repository) GetSelfGameStakePool(ctx context.Context, appCode string, gameID string, stakeCoin int64, nowMs int64) (selfgame.StakePool, error) {
	app := appcode.Normalize(appCode)
	gameID = normalizeDiceGameID(gameID)
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, game_id, stake_coin, balance_coin, reserved_coin,
		        target_balance_coin, safe_balance_coin, warning_balance_coin, danger_balance_coin,
		        status, created_at_ms, updated_at_ms
		   FROM game_self_game_stake_pools
		  WHERE app_code = ? AND game_id = ? AND stake_coin = ?`,
		app, gameID, stakeCoin,
	)
	var pool selfgame.StakePool
	if err := row.Scan(
		&pool.AppCode, &pool.GameID, &pool.StakeCoin, &pool.BalanceCoin, &pool.ReservedCoin,
		&pool.TargetBalanceCoin, &pool.SafeBalanceCoin, &pool.WarningBalanceCoin, &pool.DangerBalanceCoin,
		&pool.Status, &pool.CreatedAtMS, &pool.UpdatedAtMS,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return selfgame.DefaultStakePool(app, gameID, stakeCoin, nowMs), nil
		}
		return selfgame.StakePool{}, err
	}
	return selfgame.NormalizeStakePool(pool), nil
}

func (r *Repository) ListSelfGameStakePools(ctx context.Context, appCode string, gameID string, nowMs int64) ([]selfgame.StakePool, error) {
	app := appcode.Normalize(appCode)
	gameID = normalizeDiceGameID(gameID)
	rows, err := r.db.QueryContext(ctx,
		`SELECT app_code, game_id, stake_coin, balance_coin, reserved_coin,
		        target_balance_coin, safe_balance_coin, warning_balance_coin, danger_balance_coin,
		        status, created_at_ms, updated_at_ms
		   FROM game_self_game_stake_pools
		  WHERE app_code = ? AND game_id = ?
		  ORDER BY stake_coin ASC`,
		app, gameID,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	pools := make([]selfgame.StakePool, 0)
	for rows.Next() {
		var pool selfgame.StakePool
		if err := rows.Scan(
			&pool.AppCode, &pool.GameID, &pool.StakeCoin, &pool.BalanceCoin, &pool.ReservedCoin,
			&pool.TargetBalanceCoin, &pool.SafeBalanceCoin, &pool.WarningBalanceCoin, &pool.DangerBalanceCoin,
			&pool.Status, &pool.CreatedAtMS, &pool.UpdatedAtMS,
		); err != nil {
			return nil, err
		}
		pools = append(pools, selfgame.NormalizeStakePool(pool))
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return pools, nil
}

func (r *Repository) UpsertSelfGameStakePoolConfig(ctx context.Context, pool selfgame.StakePool, nowMs int64) (selfgame.StakePool, error) {
	pool = selfgame.NormalizeStakePool(pool)
	pool.AppCode = appcode.Normalize(pool.AppCode)
	pool.GameID = normalizeDiceGameID(pool.GameID)
	if pool.CreatedAtMS <= 0 {
		pool.CreatedAtMS = nowMs
	}
	pool.UpdatedAtMS = nowMs
	if pool.Status == "" {
		pool.Status = "active"
	}
	// 档位奖池配置只覆盖阈值和状态；余额、冻结金额必须继续由档位调账和结算流水串行修改。
	// 这样后台误保存配置不会绕过流水审计，也不会把并发中的 reserved_coin 清零。
	if _, err := r.db.ExecContext(ctx,
		`INSERT INTO game_self_game_stake_pools (
		        app_code, game_id, stake_coin, balance_coin, reserved_coin,
		        target_balance_coin, safe_balance_coin, warning_balance_coin, danger_balance_coin,
		        status, created_at_ms, updated_at_ms
		  ) VALUES (?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?)
		  ON DUPLICATE KEY UPDATE
		        target_balance_coin = VALUES(target_balance_coin),
		        safe_balance_coin = VALUES(safe_balance_coin),
		        warning_balance_coin = VALUES(warning_balance_coin),
		        danger_balance_coin = VALUES(danger_balance_coin),
		        status = VALUES(status),
		        updated_at_ms = VALUES(updated_at_ms)`,
		pool.AppCode, pool.GameID, pool.StakeCoin,
		pool.TargetBalanceCoin, pool.SafeBalanceCoin, pool.WarningBalanceCoin, pool.DangerBalanceCoin,
		pool.Status, pool.CreatedAtMS, pool.UpdatedAtMS,
	); err != nil {
		return selfgame.StakePool{}, err
	}
	return r.GetSelfGameStakePool(ctx, pool.AppCode, pool.GameID, pool.StakeCoin, nowMs)
}

func (r *Repository) GetSelfGameNewUserPolicy(ctx context.Context, appCode string, gameID string, nowMs int64) (selfgame.NewUserPolicy, error) {
	app := appcode.Normalize(appCode)
	gameID = normalizeDiceGameID(gameID)
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, game_id, enabled, protection_rounds, protection_hours, max_stake_coin,
		        lifetime_subsidy_quota_coin, day1_subsidy_quota_coin, single_round_subsidy_cap_coin,
		        strategy_level, lose_streak_protection_enabled, lose_streak_trigger, normal_phase_target_win_rate_percent, black_pool_robot_force_win_enabled,
		        created_at_ms, updated_at_ms
		   FROM game_self_game_new_user_policies
		  WHERE app_code = ? AND game_id = ?`,
		app, gameID,
	)
	var policy selfgame.NewUserPolicy
	if err := row.Scan(
		&policy.AppCode, &policy.GameID, &policy.Enabled, &policy.ProtectionRounds, &policy.ProtectionHours, &policy.MaxStakeCoin,
		&policy.LifetimeSubsidyQuotaCoin, &policy.Day1SubsidyQuotaCoin, &policy.SingleRoundSubsidyCap,
		&policy.StrategyLevel, &policy.LoseStreakProtectionEnabled, &policy.LoseStreakTrigger, &policy.NormalPhaseTargetWinRatePercent, &policy.BlackPoolRobotForceWinEnabled,
		&policy.CreatedAtMS, &policy.UpdatedAtMS,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return selfgame.DefaultNewUserPolicy(app, gameID, nowMs), nil
		}
		return selfgame.NewUserPolicy{}, err
	}
	return selfgame.NormalizeNewUserPolicy(policy), nil
}

func (r *Repository) UpsertSelfGameNewUserPolicy(ctx context.Context, policy selfgame.NewUserPolicy, nowMs int64) (selfgame.NewUserPolicy, error) {
	policy.AppCode = appcode.Normalize(policy.AppCode)
	policy.GameID = normalizeDiceGameID(policy.GameID)
	policy = selfgame.NormalizeNewUserPolicy(policy)
	if policy.CreatedAtMS <= 0 {
		policy.CreatedAtMS = nowMs
	}
	policy.UpdatedAtMS = nowMs

	// 新手策略是运营可调的体验保护配置；这里按 app_code+game_id 幂等覆盖，
	// 不读取旧胜率或旧消耗表，避免后台保存一次后又把已经废弃的策略带回运行时。
	if _, err := r.db.ExecContext(ctx,
		`INSERT INTO game_self_game_new_user_policies (
		        app_code, game_id, enabled, protection_rounds, protection_hours, max_stake_coin,
		        lifetime_subsidy_quota_coin, day1_subsidy_quota_coin, single_round_subsidy_cap_coin,
			        strategy_level, lose_streak_protection_enabled, lose_streak_trigger, normal_phase_target_win_rate_percent, black_pool_robot_force_win_enabled,
			        created_at_ms, updated_at_ms
			  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		  ON DUPLICATE KEY UPDATE
		        enabled = VALUES(enabled),
		        protection_rounds = VALUES(protection_rounds),
		        protection_hours = VALUES(protection_hours),
		        max_stake_coin = VALUES(max_stake_coin),
		        lifetime_subsidy_quota_coin = VALUES(lifetime_subsidy_quota_coin),
		        day1_subsidy_quota_coin = VALUES(day1_subsidy_quota_coin),
		        single_round_subsidy_cap_coin = VALUES(single_round_subsidy_cap_coin),
		        strategy_level = VALUES(strategy_level),
			        lose_streak_protection_enabled = VALUES(lose_streak_protection_enabled),
			        lose_streak_trigger = VALUES(lose_streak_trigger),
			        normal_phase_target_win_rate_percent = VALUES(normal_phase_target_win_rate_percent),
			        black_pool_robot_force_win_enabled = VALUES(black_pool_robot_force_win_enabled),
			        updated_at_ms = VALUES(updated_at_ms)`,
		policy.AppCode, policy.GameID, policy.Enabled, policy.ProtectionRounds, policy.ProtectionHours, policy.MaxStakeCoin,
		policy.LifetimeSubsidyQuotaCoin, policy.Day1SubsidyQuotaCoin, policy.SingleRoundSubsidyCap,
		policy.StrategyLevel, policy.LoseStreakProtectionEnabled, policy.LoseStreakTrigger, policy.NormalPhaseTargetWinRatePercent, policy.BlackPoolRobotForceWinEnabled,
		policy.CreatedAtMS, policy.UpdatedAtMS,
	); err != nil {
		return selfgame.NewUserPolicy{}, err
	}
	return r.GetSelfGameNewUserPolicy(ctx, policy.AppCode, policy.GameID, nowMs)
}

func (r *Repository) GetSelfGameProtectionState(ctx context.Context, appCode string, gameID string, userID int64, nowMs int64) (selfgame.ProtectionState, error) {
	app := appcode.Normalize(appCode)
	gameID = normalizeDiceGameID(gameID)
	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, game_id, user_id, used_rounds, lifetime_subsidy_coin, day1_subsidy_coin,
		        status, first_applied_at_ms, last_applied_at_ms, created_at_ms, updated_at_ms
		   FROM game_self_game_user_protection_states
		  WHERE app_code = ? AND game_id = ? AND user_id = ?`,
		app, gameID, userID,
	)
	var state selfgame.ProtectionState
	if err := row.Scan(
		&state.AppCode, &state.GameID, &state.UserID, &state.UsedRounds, &state.LifetimeSubsidyCoin, &state.Day1SubsidyCoin,
		&state.Status, &state.FirstAppliedAtMS, &state.LastAppliedAtMS, &state.CreatedAtMS, &state.UpdatedAtMS,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return selfgame.NormalizeProtectionState(selfgame.ProtectionState{
				AppCode:     app,
				GameID:      gameID,
				UserID:      userID,
				Status:      "active",
				CreatedAtMS: nowMs,
				UpdatedAtMS: nowMs,
			}), nil
		}
		return selfgame.ProtectionState{}, err
	}
	return selfgame.NormalizeProtectionState(state), nil
}

func (r *Repository) GetSelfGameRiskResult(ctx context.Context, appCode string, gameID string, userID int64, nowMs int64) (selfgame.RiskResult, error) {
	app := appcode.Normalize(appCode)
	gameID = normalizeDiceGameID(gameID)
	var enabled bool
	var protectionProfitLimit int64
	err := r.db.QueryRowContext(ctx,
		`SELECT enabled, protection_profit_limit_coin
		   FROM game_self_game_risk_rules
		  WHERE app_code = ? AND game_id = ?`,
		app, gameID,
	).Scan(&enabled, &protectionProfitLimit)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return selfgame.RiskResult{}, err
	}
	if errors.Is(err, sql.ErrNoRows) || !enabled {
		return selfgame.NormalizeRiskResult(selfgame.RiskResult{}), nil
	}
	state, err := r.GetSelfGameProtectionState(ctx, app, gameID, userID, nowMs)
	if err != nil {
		return selfgame.RiskResult{}, err
	}
	result := selfgame.RiskResult{}
	reason := map[string]any{"protection_profit_limit_coin": protectionProfitLimit, "lifetime_subsidy_coin": state.LifetimeSubsidyCoin}
	if protectionProfitLimit > 0 && state.LifetimeSubsidyCoin >= protectionProfitLimit {
		result.RiskScore = 70
		result.BlockProtection = true
		result.BlockReason = "protection_profit_limit"
		reason["hit"] = true
	}
	raw, _ := json.Marshal(reason)
	result.ReasonJSON = string(raw)
	return selfgame.NormalizeRiskResult(result), nil
}

func (r *Repository) GetSelfGameUserStats(ctx context.Context, appCode string, gameID string, userID int64, nowMs int64) (selfgame.UserStats, error) {
	app := appcode.Normalize(appCode)
	gameID = normalizeDiceGameID(gameID)
	dayStartMS := nowMs - nowMs%86_400_000
	sevenDayStartMS := nowMs - 7*86_400_000
	stats := selfgame.UserStats{}

	// 有效机器人局只统计已结算、非和局、真人参与者的机器人局；rock 和局不在这里出现，dice 和局会重投不落 settled。
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*)
		   FROM game_dice_matches m
		   JOIN game_dice_participants p ON p.app_code = m.app_code AND p.match_id = m.match_id
		  WHERE m.app_code = ? AND m.game_id = ? AND m.match_mode = ? AND m.status = ?
		    AND m.result <> ? AND p.user_id = ? AND p.participant_type = ?`,
		app, gameID, dicedomain.MatchModeRobot, dicedomain.MatchStatusSettled,
		dicedomain.ParticipantResultDraw, userID, dicedomain.ParticipantTypeUser,
	).Scan(&stats.EffectiveBotGameCount); err != nil {
		return selfgame.UserStats{}, err
	}
	// 今日机器人局数仍只用于机器人补贴节奏；资金盈亏和连赢连输要覆盖真人局和机器人局，
	// 因为新手期用户的真实体验来自所有自研游戏结算结果，而不是只来自机器人补位结果。
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*)
		   FROM game_dice_matches m
		   JOIN game_dice_participants p ON p.app_code = m.app_code AND p.match_id = m.match_id
		  WHERE m.app_code = ? AND m.game_id = ? AND m.match_mode = ? AND m.status = ?
		    AND m.result <> ? AND p.user_id = ? AND p.participant_type = ? AND m.settled_at_ms >= ?`,
		app, gameID, dicedomain.MatchModeRobot, dicedomain.MatchStatusSettled,
		dicedomain.ParticipantResultDraw, userID, dicedomain.ParticipantTypeUser, dayStartMS,
	).Scan(&stats.TodayRobotGameCount); err != nil {
		return selfgame.UserStats{}, err
	}
	if err := r.db.QueryRowContext(ctx,
		`SELECT COALESCE(SUM(CASE
		          WHEN p.result = ? THEN p.payout_coin - p.stake_coin
		          WHEN p.result = ? THEN -p.stake_coin
		          ELSE 0
		        END), 0)
		   FROM game_dice_matches m
		   JOIN game_dice_participants p ON p.app_code = m.app_code AND p.match_id = m.match_id
		  WHERE m.app_code = ? AND m.game_id = ? AND m.status = ?
		    AND m.result <> ? AND p.user_id = ? AND p.participant_type = ? AND m.settled_at_ms >= ?`,
		dicedomain.ParticipantResultWin, dicedomain.ParticipantResultLose,
		app, gameID, dicedomain.MatchStatusSettled,
		dicedomain.ParticipantResultDraw, userID, dicedomain.ParticipantTypeUser, dayStartMS,
	).Scan(&stats.TodayNetWinCoin); err != nil {
		return selfgame.UserStats{}, err
	}
	if err := r.db.QueryRowContext(ctx,
		`SELECT COALESCE(SUM(CASE
		          WHEN p.result = ? THEN p.payout_coin - p.stake_coin
		          WHEN p.result = ? THEN -p.stake_coin
		          ELSE 0
		        END), 0)
		   FROM game_dice_matches m
		   JOIN game_dice_participants p ON p.app_code = m.app_code AND p.match_id = m.match_id
		  WHERE m.app_code = ? AND m.game_id = ? AND m.status = ?
		    AND m.result <> ? AND p.user_id = ? AND p.participant_type = ? AND m.settled_at_ms >= ?`,
		dicedomain.ParticipantResultWin, dicedomain.ParticipantResultLose,
		app, gameID, dicedomain.MatchStatusSettled,
		dicedomain.ParticipantResultDraw, userID, dicedomain.ParticipantTypeUser, sevenDayStartMS,
	).Scan(&stats.SevenDayNetWinCoin); err != nil {
		return selfgame.UserStats{}, err
	}
	if err := r.fillSelfGameStreaks(ctx, app, gameID, userID, &stats); err != nil {
		return selfgame.UserStats{}, err
	}
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*), COALESCE(MAX(created_at_ms), 0)
		   FROM game_self_game_strategy_logs
		  WHERE app_code = ? AND game_id = ? AND user_id = ?
		    AND decision = ? AND created_at_ms >= ?`,
		app, gameID, userID, selfgame.DecisionForceHumanWin, dayStartMS,
	).Scan(&stats.TodayInterventions, &stats.LastInterventionAtMS); err != nil {
		return selfgame.UserStats{}, err
	}
	return stats, nil
}

func (r *Repository) fillSelfGameStreaks(ctx context.Context, app string, gameID string, userID int64, stats *selfgame.UserStats) error {
	rows, err := r.db.QueryContext(ctx,
		`SELECT p.result
		   FROM game_dice_matches m
		   JOIN game_dice_participants p ON p.app_code = m.app_code AND p.match_id = m.match_id
		  WHERE m.app_code = ? AND m.game_id = ? AND m.status = ?
		    AND m.result <> ? AND p.user_id = ? AND p.participant_type = ?
		  ORDER BY m.settled_at_ms DESC, m.match_id DESC
		  LIMIT 20`,
		app, gameID, dicedomain.MatchStatusSettled,
		dicedomain.ParticipantResultDraw, userID, dicedomain.ParticipantTypeUser,
	)
	if err != nil {
		return err
	}
	defer rows.Close()
	var firstResult string
	for rows.Next() {
		var result string
		if err := rows.Scan(&result); err != nil {
			return err
		}
		result = strings.TrimSpace(result)
		if firstResult == "" {
			firstResult = result
		}
		if result != firstResult {
			break
		}
		if result == dicedomain.ParticipantResultWin {
			stats.WinStreak++
		}
		if result == dicedomain.ParticipantResultLose {
			stats.LoseStreak++
		}
	}
	return rows.Err()
}

func (r *Repository) RecordSelfGameStrategyLog(ctx context.Context, log selfgame.StrategyLog) error {
	log.AppCode = appcode.Normalize(log.AppCode)
	log.GameID = normalizeDiceGameID(log.GameID)
	log.ReasonJSON = strings.TrimSpace(log.ReasonJSON)
	if log.ReasonJSON == "" {
		log.ReasonJSON = "{}"
	}
	_, err := r.db.ExecContext(ctx,
		`INSERT INTO game_self_game_strategy_logs (
		   app_code, log_id, game_id, stake_coin, match_id, user_id, strategy_code, decision,
		   force_result, reason_code, reason_json, pool_balance_coin, pool_level, created_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?)
		 ON DUPLICATE KEY UPDATE
		   strategy_code = VALUES(strategy_code),
		   decision = VALUES(decision),
		   force_result = VALUES(force_result),
		   reason_code = VALUES(reason_code),
		   reason_json = VALUES(reason_json),
		   pool_balance_coin = VALUES(pool_balance_coin),
		   pool_level = VALUES(pool_level)`,
		log.AppCode, log.LogID, log.GameID, log.StakeCoin, log.MatchID, log.UserID, log.StrategyCode, log.Decision,
		log.ForceResult, log.ReasonCode, log.ReasonJSON, log.PoolBalanceCoin, log.PoolLevel, log.CreatedAtMS,
	)
	return err
}

func (r *Repository) ConsumeSelfGameProtection(ctx context.Context, event selfgame.ProtectionEvent) (selfgame.ProtectionState, bool, error) {
	event.AppCode = appcode.Normalize(event.AppCode)
	event.GameID = normalizeDiceGameID(event.GameID)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return selfgame.ProtectionState{}, false, err
	}
	defer func() { _ = tx.Rollback() }()

	res, err := tx.ExecContext(ctx,
		`INSERT IGNORE INTO game_self_game_user_protection_events (
		   app_code, game_id, match_id, user_id, used_round_delta,
		   lifetime_subsidy_coin_delta, day1_subsidy_coin_delta, created_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
		event.AppCode, event.GameID, event.MatchID, event.UserID, event.UsedRoundDelta,
		event.LifetimeSubsidyCoinDelta, event.Day1SubsidyCoinDelta, event.CreatedAtMS,
	)
	if err != nil {
		return selfgame.ProtectionState{}, false, err
	}
	affected, _ := res.RowsAffected()
	if affected == 0 {
		if err := tx.Commit(); err != nil {
			return selfgame.ProtectionState{}, false, err
		}
		state, err := r.GetSelfGameProtectionState(ctx, event.AppCode, event.GameID, event.UserID, event.CreatedAtMS)
		return state, false, err
	}
	if _, err := tx.ExecContext(ctx,
		`INSERT INTO game_self_game_user_protection_states (
		   app_code, game_id, user_id, used_rounds, lifetime_subsidy_coin, day1_subsidy_coin,
		   status, first_applied_at_ms, last_applied_at_ms, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, 0, 0, 0, 'active', 0, 0, ?, ?)
		 ON DUPLICATE KEY UPDATE updated_at_ms = updated_at_ms`,
		event.AppCode, event.GameID, event.UserID, event.CreatedAtMS, event.CreatedAtMS,
	); err != nil {
		return selfgame.ProtectionState{}, false, err
	}
	if _, err := tx.ExecContext(ctx,
		`UPDATE game_self_game_user_protection_states
		    SET used_rounds = used_rounds + ?,
		        lifetime_subsidy_coin = lifetime_subsidy_coin + ?,
		        day1_subsidy_coin = day1_subsidy_coin + ?,
		        first_applied_at_ms = IF(first_applied_at_ms = 0, ?, first_applied_at_ms),
		        last_applied_at_ms = ?,
		        updated_at_ms = ?
		  WHERE app_code = ? AND game_id = ? AND user_id = ?`,
		event.UsedRoundDelta, event.LifetimeSubsidyCoinDelta, event.Day1SubsidyCoinDelta,
		event.CreatedAtMS, event.CreatedAtMS, event.CreatedAtMS, event.AppCode, event.GameID, event.UserID,
	); err != nil {
		return selfgame.ProtectionState{}, false, err
	}
	if err := tx.Commit(); err != nil {
		return selfgame.ProtectionState{}, false, err
	}
	state, err := r.GetSelfGameProtectionState(ctx, event.AppCode, event.GameID, event.UserID, event.CreatedAtMS)
	return state, true, err
}
