<?php
declare(strict_types=1);

namespace app\api\logic;

use app\api\cache\UserCache;
use app\api\util\ApiLang;
use app\api\service\LotteryService;
use app\dice\helper\AdminScopeHelper;
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use app\dice\model\play_record\DicePlayRecord;
use app\dice\model\ante_config\DiceAnteConfig;
use app\dice\model\player\DicePlayer;
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
use app\dice\model\reward\DiceReward;
use app\dice\model\reward\DiceRewardConfig;
use plugin\saiadmin\exception\ApiException;
use support\Log;
use support\think\Cache;
use support\think\Db;

/**
 * 开始游戏 / 抽奖一局
 */
class PlayStartLogic
{
    /** 钱包流水类型：购买抽奖次数 */
    public const WALLET_TYPE_BUY_DRAW = 2;
    /** 抽奖类型：付费 */
    public const LOTTERY_TYPE_PAID = 0;
    /** 抽奖类型：免费 */
    public const LOTTERY_TYPE_FREE = 1;
    /** 钱包流水类型：抽奖 */
    public const WALLET_TYPE_DRAW = 5;
    /** 对局状态：成功 */
    public const RECORD_STATUS_SUCCESS = 1;
    /** 对局状态：超时/失败 */
    public const RECORD_STATUS_TIMEOUT = 0;

    /** 单注费用（抽奖券基础费用） */
    private const UNIT_COST = 1.0;
    /** 免费抽奖注数缓存 key 前缀（用于强制下一局注数一致） */
    private const FREE_ANTE_KEY_PREFIX = 'api:game:free_ante:';
    /** 免费抽奖注数缓存过期（秒） */
    private const FREE_ANTE_TTL = 86400 * 7;
    /** 豹子号中大奖额外平台币（无 BIGWIN 配置时兜底） */
    private const SUPER_WIN_BONUS = 500;
    /** 可触发超级大奖的 grid_number（5=全1 10=全2 15=全3 20=全4 25=全5 30=全6） */
    private const SUPER_WIN_GRID_NUMBERS = [5, 10, 15, 20, 25, 30];
    /** 5 和 30 抽到即豹子，不参与 BIGWIN 权重判定；10/15/20/25 按 BIGWIN weight 判定是否豹子 */
    private const SUPER_WIN_ALWAYS_GRID_NUMBERS = [5, 30];
    /** T4 惩罚格余额不足时写入游玩记录的备注 */
    private const REMARK_T4_INSUFFICIENT_BALANCE = '惩罚格奖励：玩家余额不足，已扣尽钱包剩余余额';

    /**
     * 执行一局游戏
     * @param int $playerId 玩家ID
     * @param int $direction 方向 0=无/顺时针 1=中奖/逆时针（前端 direction）
     * @param int $ante 注数（必须在 DiceAnteConfig.mult 中存在）
     * @return array 成功返回 DicePlayRecord 数据；余额不足时抛 ApiException，message 为约定文案
     */
    public function run(int $playerId, int $direction, int $ante): array
    {
        $player = DicePlayer::find($playerId);
        if (!$player) {
            throw new ApiException('User not found');
        }

        $configDeptId = AdminScopeHelper::resolvePlayerConfigDeptId($player);
        $playerDeptId = isset($player->dept_id) && $player->dept_id !== null && $player->dept_id !== ''
            ? (int) $player->dept_id
            : null;

        $coin = (float) $player->coin;
        if ($ante <= 0) {
            throw new ApiException('ante must be a positive integer');
        }

        // 注数合规校验：ante 必须存在于 dice_ante_config.mult
        $anteConfigModel = new DiceAnteConfig();
        $exists = $anteConfigModel->where('mult', $ante)->where('dept_id', $configDeptId)->count();
        if ($exists <= 0) {
            throw new ApiException('当前注数不合规，请选择正确的注数');
        }

        // 免费抽奖：优先使用 free_ticket（带 ante 与 count）；兼容旧字段 free_ticket_count
        $freeTicket = $player->free_ticket ?? null;
        $freeTicketAnte = null;
        $freeTicketCount = 0;
        if (is_array($freeTicket)) {
            $a = $freeTicket['ante'] ?? null;
            $c = $freeTicket['count'] ?? null;
            if ($a !== null && $a !== '' && is_numeric($a)) {
                $freeTicketAnte = (int) $a;
            }
            if ($c !== null && $c !== '' && is_numeric($c)) {
                $freeTicketCount = (int) $c;
            }
        }
        $legacyFreeCount = (int) ($player->free_ticket_count ?? 0);
        $isFree = ($freeTicketAnte !== null && $freeTicketCount > 0) || $legacyFreeCount > 0;
        $ticketType = $isFree ? self::LOTTERY_TYPE_FREE : self::LOTTERY_TYPE_PAID;

        // 若为 free_ticket 免费抽奖：注数必须与券的 ante 一致
        if ($ticketType === self::LOTTERY_TYPE_FREE && $freeTicketAnte !== null && $freeTicketCount > 0) {
            if ($ante !== $freeTicketAnte) {
                throw new ApiException('您有一张底注为' . $freeTicketAnte . '的免费抽奖券');
            }
        }

        // 若为免费抽奖（旧逻辑）：注数必须与上一次触发免费抽奖时的注数一致
        if ($isFree) {
            $requiredAnte = Cache::get(self::FREE_ANTE_KEY_PREFIX . $playerId);
            if ($requiredAnte !== null && $requiredAnte !== '' && (int) $requiredAnte !== $ante) {
                throw new ApiException('免费抽奖注数必须与上一次一致，请修改注数后继续');
            }
        }

        $configType0 = DiceLotteryPoolConfig::where('name', 'default')->where('dept_id', $configDeptId)->find();
        $configKill = DiceLotteryPoolConfig::where('name', 'killScore')->where('dept_id', $configDeptId)->find();
        $configFree = DiceLotteryPoolConfig::where('name', 'free')->where('dept_id', $configDeptId)->find();
        if (!$configType0) {
            throw new ApiException('Lottery pool config not found (name=default required)');
        }

        // 付费抽奖：开始前扣除费用 ante * UNIT_COST
        $paidAmount = $ticketType === self::LOTTERY_TYPE_PAID ? round($ante * self::UNIT_COST, 2) : 0.0;

        // 付费抽奖：余额不足单局费用（ante * UNIT_COST）时不允许开始；惩罚格不足部分在局内扣尽剩余余额
        if ($ticketType === self::LOTTERY_TYPE_PAID && $coin < $paidAmount) {
            throw new ApiException('余额不足');
        }

        // 彩金池累计盈利：用于判断是否触发杀分（不再依赖单个玩家累计盈利）
        // 该值来自 dice_lottery_pool_config.profit_amount（default 奖池）
        $poolProfitTotal = $configType0->profit_amount ?? 0;
        $safetyLine = (int) ($configType0->safety_line ?? 0);
        $killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1;

        $usePaidKill = $ticketType === self::LOTTERY_TYPE_PAID
            && $killEnabled
            && $poolProfitTotal >= $safetyLine
            && $configKill !== null;

        $playerLinkedPool = $this->resolvePlayerLinkedPoolConfig($player, $configDeptId);

        if ($ticketType === self::LOTTERY_TYPE_FREE) {
            // 免费抽奖券：使用本渠道 name=free 奖池档位权重；无 free 时回退 default
            $config = $configFree ?? $configType0;
            $usePoolWeights = true;
        } elseif ($usePaidKill) {
            $config = $configKill;
            $usePoolWeights = true;
        } else {
            // 付费未触发杀分：关联 playerDefault 时实时读该池权重；否则按玩家行内 T*_weight
            $config = $configType0;
            $usePoolWeights = false;
            if ($playerLinkedPool !== null && $playerLinkedPool->isPlayerDefaultTemplate()) {
                $usePoolWeights = true;
                $config = $playerLinkedPool;
            }
        }

        // 按档位 T1-T5 抽取后，从 DiceReward 表按当前方向取该档位数据，再按 weight 抽取一条得到 grid_number
        $rewardInstance = DiceReward::getCachedInstance($configDeptId);
        $byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
        $maxTierRetry = 10;
        $chosen = null;
        $tier = null;
        for ($tierAttempt = 0; $tierAttempt < $maxTierRetry; $tierAttempt++) {
            $tier = $usePoolWeights
                ? LotteryService::drawTierByWeights($config)
                : LotteryService::drawTierByPlayerWeights($player);
            $tierRewards = $byTierDirection[$tier][$direction] ?? [];
            if (empty($tierRewards)) {
                Log::warning("档位 {$tier} 方向 {$direction} 无任何 DiceReward，重新摇取档位");
                continue;
            }
            if ($usePoolWeights) {
                $tierRewards = self::filterOutSuperWinOnlyGrids($tierRewards);
                if (empty($tierRewards)) {
                    Log::warning("档位 {$tier} 方向 {$direction} 杀分档位下排除 5/30 后无可用奖励，重新摇取档位");
                    continue;
                }
            }
            try {
                $chosen = self::drawRewardByWeight($tierRewards);
            } catch (\RuntimeException $e) {
                if ($e->getMessage() === self::EXCEPTION_WEIGHT_ALL_ZERO) {
                    Log::warning("档位 {$tier} 下所有奖励权重均为 0，重新摇取档位");
                    continue;
                }
                throw $e;
            }
            break;
        }
        if ($chosen === null) {
            Log::error("多次摇取档位后仍无有效 DiceReward");
            throw new ApiException('No available reward config');
        }

        $startIndex = (int) ($chosen['start_index'] ?? 0);
        $targetIndex = (int) ($chosen['end_index'] ?? 0);
        $rollNumber = (int) ($chosen['grid_number'] ?? 0);
        $realEv = (float) ($chosen['real_ev'] ?? 0);
        // T5/再来一次：以奖励行 tier 为准，并以摇奖档位 $tier 兜底（与 reward_tier 展示一致，避免 dice_reward 行缺 tier 时不发券）
        $isTierPlayAgain = (string) ($chosen['tier'] ?? '') === 'T5';
        if ($isTierPlayAgain === false && (string) ($tier ?? '') === 'T5') {
            $isTierPlayAgain = true;
        }
        // 摇色子中奖：按 dice_reward_config.real_ev 直接结算（已乘 ante）
        $rewardWinCoin = round($realEv * $ante, 2);

        // 豹子判定：5/30 必豹子；10/15/20/25 按 DiceRewardConfig 中 BIGWIN 该点数的 weight 判定（0-10000，10000=100%）
        // 杀分档位：不触发豹子，5/30 已在上方抽取时排除，10/15/20/25 仅生成非豹子组合
        $superWinCoin = 0.0;
        $isWin = 0;
        $bigWinRealEv = 0.0;
        if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) {
            if ($usePoolWeights) {
                // 杀分档位：绝不触发豹子，仅生成非豹子组合，不发放豹子奖金
                $isWin = 0;
                $superWinCoin = 0.0;
                $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
            } else {
                $bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber, $configDeptId);
                $alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true);
                $doSuperWin = $alwaysSuperWin;
                if (!$doSuperWin) {
                    $bigWinWeight = 10000;
                    if ($bigWinConfig !== null && isset($bigWinConfig['weight'])) {
                        $bigWinWeight = min(10000, max(0, (int) $bigWinConfig['weight']));
                        $bigWinRealEv = (float) ($bigWinConfig['real_ev'] ?? 0);
                    }
                    $roll = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX);
                    $doSuperWin = $bigWinWeight > 0 && $roll < ($bigWinWeight / 10000.0);
                } else {
                    if ($bigWinConfig !== null) {
                        $bigWinRealEv = (float) ($bigWinConfig['real_ev'] ?? 0);
                    }
                }
                if ($doSuperWin) {
                    $rollArray = $this->getSuperWinRollArray($rollNumber);
                    $isWin = 1;
                    $bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS;
                    $superWinCoin = round($bigWinEv * $ante, 2);
                    // 中 BIGWIN 豹子：不走原奖励流程，不记录原奖励，不触发 T5 再来一次，仅发放豹子奖金
                    $rewardWinCoin = 0.0;
                    $realEv = 0.0;
                    $isTierPlayAgain = false;
                } else {
                    $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
                }
            }
        } else {
            $rollArray = $this->generateRollArrayFromSum($rollNumber);
        }

        Log::info(sprintf(
            '摇取点数 roll_number=%d, 方向=%d, start_index=%d, target_index=%d',
            $rollNumber,
            $direction,
            $startIndex,
            $targetIndex
        ));
        $winCoin = round($superWinCoin + $rewardWinCoin, 2); // 赢取平台币 = 中大奖 + 摇色子中奖（豹子时 rewardWinCoin 已为 0）

        $record = null;
        $settledWinCoin = $winCoin;
        $configId = (int) $config->id;
        if ($ticketType === self::LOTTERY_TYPE_PAID && !$usePaidKill && $playerLinkedPool !== null) {
            $configId = (int) $playerLinkedPool->id;
        }
        $type0ConfigId = (int) $configType0->id;
        $rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex; // 中豹子不记录原奖励配置 id
        $configName = (string) ($config->name ?? '');
        $adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
        try {
            Db::transaction(function () use (
                $playerId,
                $playerDeptId,
                $adminId,
                $configId,
                $type0ConfigId,
                $configName,
                $ticketType,
                $ante,
                $paidAmount,
                $winCoin,
                $superWinCoin,
                $rewardWinCoin,
                $isWin,
                $realEv,
                $bigWinRealEv,
                $direction,
                $startIndex,
                $targetIndex,
                $rollArray,
                $isTierPlayAgain,
                $tier,
                &$record,
                &$settledWinCoin
            ) {
                $rewardTier = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? '');

                $p = DicePlayer::find($playerId);
                if (!$p) {
                    throw new \RuntimeException('玩家不存在');
                }
                $coinBefore = (float) $p->coin;
                // 开始前先扣付费金额，再加中奖金额（免费抽奖 paid_amount=0）
                $coinAfter = round($coinBefore - $paidAmount + $winCoin, 2);
                $recordWinCoin = $winCoin;
                $recordRewardWinCoin = $rewardWinCoin;
                $playRecordRemark = null;
                // T4 惩罚：扣完购券费用后若不足以支付全额惩罚，则扣尽钱包剩余余额并记录备注
                if ($rewardTier === 'T4' && $coinAfter < 0) {
                    $walletRemain = round($coinBefore - $paidAmount, 2);
                    $coinAfter = 0.0;
                    $recordWinCoin = round(-$walletRemain, 2);
                    $recordRewardWinCoin = $recordWinCoin;
                    $playRecordRemark = self::REMARK_T4_INSUFFICIENT_BALANCE;
                }
                $settledWinCoin = $recordWinCoin;

                $record = DicePlayRecord::create([
                    'player_id'          => $playerId,
                    'dept_id'            => $playerDeptId,
                    'admin_id'           => $adminId,
                    'lottery_config_id'  => $configId,
                    'lottery_type'       => $ticketType,
                    'ante'               => $ante,
                    'paid_amount'        => $paidAmount,
                    'is_win'             => $isWin,
                    'win_coin'           => $recordWinCoin,
                    'super_win_coin'     => $superWinCoin,
                    'reward_win_coin'    => $recordRewardWinCoin,
                    'direction'          => $direction,
                    'reward_tier'        => $rewardTier,
                    'start_index'        => $startIndex,
                    'target_index'       => $targetIndex,
                    'roll_array'         => is_array($rollArray) ? json_encode($rollArray) : $rollArray,
                    'roll_number'        => is_array($rollArray) ? array_sum($rollArray) : 0,
                    'status'             => self::RECORD_STATUS_SUCCESS,
                    'remark'             => $playRecordRemark,
                ]);

                $p->coin = $coinAfter;
                // 免费抽奖消耗：优先消耗 free_ticket.count，耗尽则清空 free_ticket；否则兼容旧 free_ticket_count
                if ($ticketType === self::LOTTERY_TYPE_FREE) {
                    $ft = $p->free_ticket ?? null;
                    $ftAnte = null;
                    $ftCount = 0;
                    if (is_array($ft)) {
                        $a = $ft['ante'] ?? null;
                        $c = $ft['count'] ?? null;
                        if ($a !== null && $a !== '' && is_numeric($a)) {
                            $ftAnte = (int) $a;
                        }
                        if ($c !== null && $c !== '' && is_numeric($c)) {
                            $ftCount = (int) $c;
                        }
                    }
                    if ($ftAnte !== null && $ftCount > 0) {
                        $next = $ftCount - 1;
                        if ($next <= 0) {
                            $p->free_ticket = null;
                        } else {
                            $p->free_ticket = ['ante' => $ftAnte, 'count' => $next];
                        }
                    } else {
                        $p->free_ticket_count = max(0, (int) $p->free_ticket_count - 1);
                    }
                }

                // 记录每次游玩：写入抽奖券记录（用于后台“抽奖券记录”追踪付费/免费游玩与消耗）
                $isPaidPlay = $ticketType === self::LOTTERY_TYPE_PAID;
                $paidCnt = $isPaidPlay ? 1 : 0;
                $freeCnt = $isPaidPlay ? 0 : 1;
                DicePlayerTicketRecord::create([
                    'player_id'          => $playerId,
                    'dept_id'            => $playerDeptId,
                    'admin_id'           => $adminId,
                    'use_coins'          => $paidAmount,
                    'ante'               => $ante,
                    'total_ticket_count' => $paidCnt + $freeCnt,
                    'paid_ticket_count'  => $paidCnt,
                    'free_ticket_count'  => $freeCnt,
                    'remark'             => ($isPaidPlay ? '付费游玩' : '免费游玩') . '|play_record_id=' . $record->id,
                ]);

                // 若本局中奖档位为 T5，则额外赠送 1 次免费抽奖次数：
                // - 新结构：写入 free_ticket（ante=本局注数，count+1）
                // - 兼容旧结构：free_ticket_count +1
                if ($isTierPlayAgain) {
                    $ft = $p->free_ticket ?? null;
                    $ftAnte = null;
                    $ftCount = 0;
                    if (is_array($ft)) {
                        $a = $ft['ante'] ?? null;
                        $c = $ft['count'] ?? null;
                        if ($a !== null && $a !== '' && is_numeric($a)) {
                            $ftAnte = (int) $a;
                        }
                        if ($c !== null && $c !== '' && is_numeric($c)) {
                            $ftCount = (int) $c;
                        }
                    }
                    if ($ftAnte === null) {
                        $ftAnte = $ante;
                    }
                    if ($ftAnte === $ante) {
                        $p->free_ticket = ['ante' => $ante, 'count' => $ftCount + 1];
                    } else {
                        // 若已有不同注数的免费券，则仍保留旧字段累加，避免覆盖玩家已有券
                        $p->free_ticket_count = (int) $p->free_ticket_count + 1;
                    }

                    DicePlayerTicketRecord::create([
                        'player_id'         => $playerId,
                        'dept_id'           => $playerDeptId,
                        'admin_id'          => $adminId,
                        'ante'              => $ante,
                        'free_ticket_count' => 1,
                        'remark'            => '中奖结果为T5',
                    ]);
                    // 记录免费抽奖注数，用于强制下一局注数一致
                    Cache::set(self::FREE_ANTE_KEY_PREFIX . $playerId, $ante, self::FREE_ANTE_TTL);
                } else {
                    // 若本次消耗了最后一次免费抽奖，则清理注数锁
                    $ft = $p->free_ticket ?? null;
                    $ftCount = 0;
                    if (is_array($ft)) {
                        $c = $ft['count'] ?? null;
                        if ($c !== null && $c !== '' && is_numeric($c)) {
                            $ftCount = (int) $c;
                        }
                    }
                    if ($ticketType === self::LOTTERY_TYPE_FREE && $ftCount <= 0 && (int) $p->free_ticket_count <= 0) {
                        Cache::delete(self::FREE_ANTE_KEY_PREFIX . $playerId);
                    }
                }

                $p->save();

                // 彩金池累计盈利累加在 name=default 彩金池上：
                // 付费：每局按「本局赢取平台币 win_coin - 抽奖费用 paid_amount（ante*UNIT_COST）」
                // 免费券：paid_amount=0，只计入 win_coin
                $perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($recordWinCoin - $paidAmount) : $recordWinCoin;
                $addProfit = round($perPlayProfit, 2);
                try {
                    DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([
                        'profit_amount' => Db::raw('IFNULL(profit_amount,0) + ' . sprintf('%.2f', $addProfit)),
                    ]);
                } catch (\Throwable $e) {
                    Log::warning('彩金池盈利累加失败', [
                        'config_id' => $type0ConfigId,
                        'add_profit' => $addProfit,
                        'message'    => $e->getMessage(),
                    ]);
                }

                // 钱包流水拆分：先记录购券扣费，再记录抽奖结果（中奖/惩罚）
                if ($paidAmount > 0) {
                    $walletAfterBuy = round($coinBefore - $paidAmount, 2);
                    DicePlayerWalletRecord::create([
                        'player_id'      => $playerId,
                        'dept_id'        => $playerDeptId,
                        'admin_id'       => $adminId,
                        'coin'           => round(-$paidAmount, 2),
                        'type'           => self::WALLET_TYPE_BUY_DRAW,
                        'wallet_before'  => $coinBefore,
                        'wallet_after'   => $walletAfterBuy,
                        'remark'         => '抽奖购券扣费|play_record_id=' . $record->id,
                    ]);
                }

                $walletBeforeDraw = $coinBefore - $paidAmount;
                $drawRemark = ($recordWinCoin >= 0 ? '抽奖中奖' : '抽奖惩罚') . '|play_record_id=' . $record->id;
                if ($playRecordRemark !== null) {
                    $drawRemark .= '|' . $playRecordRemark;
                }
                DicePlayerWalletRecord::create([
                    'player_id'      => $playerId,
                    'dept_id'        => $playerDeptId,
                    'admin_id'       => $adminId,
                    'coin'           => $recordWinCoin,
                    'type'           => self::WALLET_TYPE_DRAW,
                    'wallet_before'  => round($walletBeforeDraw, 2),
                    'wallet_after'   => $coinAfter,
                    'remark'         => $drawRemark,
                ]);
            });
        } catch (\Throwable $e) {
            if ($record === null) {
                try {
                    $record = DicePlayRecord::create([
                        'player_id'          => $playerId,
                        'dept_id'            => $playerDeptId,
                        'admin_id'           => $adminId ?? null,
                        'lottery_config_id'  => $configId ?? 0,
                        'lottery_type'       => $ticketType,
                        'is_win'             => 0,
                        'win_coin'           => 0,
                        'super_win_coin'     => 0,
                        'reward_win_coin'    => 0,
                        'direction'          => $direction,
                        'reward_tier'        => '',
                        'start_index'        => $startIndex,
                        'target_index'       => 0,
                        'roll_array'         => '[]',
                        'roll_number'        => 0,
                        'status'             => self::RECORD_STATUS_TIMEOUT,
                    ]);
                } catch (\Throwable $_) {
                    // 表可能无 status 字段时忽略
                }
            }
            throw $e;
        }

        $updated = DicePlayer::find($playerId);
        if ($updated) {
            UserCache::setUser($playerId, $updated->hidden(['password'])->toArray());
        }

        if (!$record instanceof DicePlayRecord) {
            throw new \RuntimeException('对局记录创建失败');
        }
        $arr = $record->toArray();
        if (isset($arr['roll_array']) && is_string($arr['roll_array'])) {
            $arr['roll_array'] = json_decode($arr['roll_array'], true) ?? [];
        }
        $arr['roll_number'] = is_array($arr['roll_array'] ?? null) ? array_sum($arr['roll_array']) : 0;
        $arr['reward_tier'] = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? '');
        // 记录完数据后返回当前玩家余额与抽奖次数
        $arr['coin'] = $updated ? round((float) $updated->coin, 2) : 0.0;
        // 本局从玩家货币中扣除的金额：付费抽奖为 ante*UNIT_COST，免费抽奖为 0（与 paid_amount 一致）
        $arr['use_coin'] = round($paidAmount, 2);
        return $arr;
    }

    /** 该组配置权重均为 0 时抛出，供调用方重试 */
    private const EXCEPTION_WEIGHT_ALL_ZERO = 'REWARD_WEIGHT_ALL_ZERO';

    /** 杀分档位需排除的豹子号：5 和 30 只能组成豹子，无法生成非豹子组合 */
    private const KILL_MODE_EXCLUDE_GRIDS = [5, 30];

    /**
     * 杀分档位下排除 grid_number=5/30 的奖励（5/30 只能豹子，无法剔除）
     * @return array 排除后的奖励列表，保持索引连续
     */
    private static function filterOutSuperWinOnlyGrids(array $rewards): array
    {
        return array_values(array_filter($rewards, function ($r) {
            $g = (int) ($r['grid_number'] ?? 0);
            return !in_array($g, self::KILL_MODE_EXCLUDE_GRIDS, true);
        }));
    }

    /**
     * 按权重抽取一条配置：仅 weight>0 参与抽取（weight=0 不会被摇到）
     * 使用 [0, total) 浮点随机，支持最小权重 0.1%（如 weight=0.1），避免整数随机导致小权重失真
     * 全部 weight 为 0 时抛出 RuntimeException(EXCEPTION_WEIGHT_ALL_ZERO)
     */
    private static function drawRewardByWeight(array $rewards): array
    {
        if (empty($rewards)) {
            throw new \InvalidArgumentException('rewards 不能为空');
        }
        $candidateWeights = [];
        foreach ($rewards as $i => $row) {
            $w = isset($row['weight']) ? (float) $row['weight'] : 0.0;
            if ($w > 0) {
                $candidateWeights[$i] = $w;
            }
        }
        $total = (float) array_sum($candidateWeights);
        if ($total > 0) {
            $r = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX) * $total;
            $acc = 0.0;
            foreach ($candidateWeights as $i => $w) {
                $acc += $w;
                if ($r < $acc) {
                    return $rewards[$i];
                }
            }
            return $rewards[array_key_last($candidateWeights)];
        }
        throw new \RuntimeException(self::EXCEPTION_WEIGHT_ALL_ZERO);
    }

    /**
     * 根据摇取点数（5-30）生成 5 个色子数组，每个 1-6，总和为 $sum
     * @return int[] 如 [1,2,3,4,5]
     */
    private function generateRollArrayFromSum(int $sum): array
    {
        $sum = max(5, min(30, $sum));
        $arr = [1, 1, 1, 1, 1];
        $remain = $sum - 5;
        for ($i = 0; $i < $remain; $i++) {
            $candidates = array_keys(array_filter($arr, function ($v) {
                return $v < 6;
            }));
            if (empty($candidates)) {
                break;
            }
            $idx = $candidates[random_int(0, count($candidates) - 1)];
            $arr[$idx]++;
        }
        shuffle($arr);
        return array_values($arr);
    }

    /**
     * 豹子组合：5->[1,1,1,1,1]，10->[2,2,2,2,2]，15->[3,3,3,3,3]，20->[4,4,4,4,4]，25->[5,5,5,5,5]，30->[6,6,6,6,6]
     * @return int[]
     */
    private function getSuperWinRollArray(int $gridNumber): array
    {
        if ($gridNumber === 30) {
            return array_fill(0, 5, 6);
        }
        $n = (int) ($gridNumber / 5);
        $n = max(1, min(5, $n));
        return array_fill(0, 5, $n);
    }

    /**
     * 生成总和为 $sum 且非豹子的 5 个色子（1-6）；sum=5 时仅 [1,1,1,1,1] 可能，仍返回该组合
     * @return int[]
     */
    private function generateNonSuperWinRollArrayWithSum(int $sum): array
    {
        $sum = max(5, min(30, $sum));
        $super = $this->getSuperWinRollArray($sum);
        if ($sum === 5) {
            return $super;
        }
        $arr = $super;
        $maxAttempts = 20;
        for ($a = 0; $a < $maxAttempts; $a++) {
            $idx = random_int(0, count($arr) - 1);
            $j = random_int(0, count($arr) - 1);
            if ($idx === $j) {
                $j = ($j + 1) % 5;
            }
            $i = $idx;
            if ($arr[$i] >= 2 && $arr[$j] <= 5) {
                $arr[$i]--;
                $arr[$j]++;
                shuffle($arr);
                return array_values($arr);
            }
            if ($arr[$i] <= 5 && $arr[$j] >= 2) {
                $arr[$i]++;
                $arr[$j]--;
                shuffle($arr);
                return array_values($arr);
            }
        }
        return $this->generateRollArrayFromSum($sum);
    }

    /**
     * 模拟一局抽奖（不写库、不扣玩家），用于权重测试写入 dice_play_record_test
     * @param \app\dice\model\lottery_pool_config\DiceLotteryPoolConfig|null $config 奖池配置，自定义档位时可为 null
     * @param int $direction 0=顺时针 1=逆时针
     * @param int $lotteryType 0=付费 1=免费
     * @param int $ante 底注/注数（dice_ante_config.mult）
     * @param array|null $customTierWeights 自定义档位权重 ['T1'=>x, 'T2'=>x, ...]，非空时忽略 config 的档位权重
     * @return array 可直接用于 DicePlayRecordTest::create 的字段 + tier（用于统计档位概率）
     */
    public function simulateOnePlay($config, int $direction, int $lotteryType = 0, int $ante = 1, ?array $customTierWeights = null, ?int $configDeptId = null): array
    {
        $useKillMode = false;
        $rewardInstance = DiceReward::getCachedInstance($configDeptId);
        $byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
        $maxTierRetry = 10;
        $chosen = null;
        $tier = null;
        for ($tierAttempt = 0; $tierAttempt < $maxTierRetry; $tierAttempt++) {
            if ($customTierWeights !== null && $customTierWeights !== []) {
                $tier = LotteryService::drawTierByWeightsFromArray($customTierWeights);
            } else {
                if ($config === null) {
                    throw new \RuntimeException('模拟抽奖：未提供奖池配置或自定义档位权重');
                }
                $tier = LotteryService::drawTierByWeights($config);
            }
            $tierRewards = $byTierDirection[$tier][$direction] ?? [];
            if (empty($tierRewards)) {
                continue;
            }
            // 免费券或 killScore 池：与实际流程一致，排除 5/30 且不触发豹子
            $useKillMode = ($lotteryType === 1) || ($config !== null && (string) ($config->name ?? '') === 'killScore');
            if ($useKillMode) {
                $tierRewards = self::filterOutSuperWinOnlyGrids($tierRewards);
                if (empty($tierRewards)) {
                    continue;
                }
            }
            try {
                $chosen = self::drawRewardByWeight($tierRewards);
            } catch (\RuntimeException $e) {
                if ($e->getMessage() === self::EXCEPTION_WEIGHT_ALL_ZERO) {
                    continue;
                }
                throw $e;
            }
            break;
        }
        if ($chosen === null) {
            throw new \RuntimeException('模拟抽奖：无可用奖励配置');
        }

        $startIndex = (int) ($chosen['start_index'] ?? 0);
        $targetIndex = (int) ($chosen['end_index'] ?? 0);
        $rollNumber = (int) ($chosen['grid_number'] ?? 0);
        $realEv = (float) ($chosen['real_ev'] ?? 0);
        // 摇色子中奖：按 real_ev 直接结算（与正式抽奖 run() 一致）
        $rewardWinCoin = round($realEv * $ante, 2);

        $superWinCoin = 0.0;
        $isWin = 0;
        $bigWinRealEv = 0.0;
        if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) {
            if ($useKillMode) {
                // 杀分档位：绝不触发豹子，仅生成非豹子组合，不发放豹子奖金
                $isWin = 0;
                $superWinCoin = 0.0;
                $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
            } else {
                $bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber, $configDeptId);
                $alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true);
                $doSuperWin = $alwaysSuperWin;
                if (!$doSuperWin) {
                    $bigWinWeight = 10000;
                    if ($bigWinConfig !== null && isset($bigWinConfig['weight'])) {
                        $bigWinWeight = min(10000, max(0, (int) $bigWinConfig['weight']));
                    }
                    $roll = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX);
                    $doSuperWin = $bigWinWeight > 0 && $roll < ($bigWinWeight / 10000.0);
                }
                if ($bigWinConfig !== null && isset($bigWinConfig['real_ev'])) {
                    $bigWinRealEv = (float) $bigWinConfig['real_ev'];
                }
                if ($doSuperWin) {
                    $rollArray = $this->getSuperWinRollArray($rollNumber);
                    $isWin = 1;
                    $bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS;
                    $superWinCoin = round($bigWinEv * $ante, 2);
                    $rewardWinCoin = 0.0;
                    // 中豹子时不走原奖励流程
                    $realEv = 0.0;
                } else {
                    $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
                }
            }
        } else {
            $rollArray = $this->generateRollArrayFromSum($rollNumber);
        }

        $winCoin = round($superWinCoin + $rewardWinCoin, 2);
        $configId = $config !== null ? (int) $config->id : 0;
        $configName = $config !== null ? (string) ($config->name ?? '') : '自定义';
        $costRealEv = $realEv + ($isWin === 1 ? $bigWinRealEv : 0.0);
        $paidAmount = $lotteryType === 0 ? round($ante * self::UNIT_COST, 2) : 0.0;
        $rewardTier = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? '');
        // 与写入记录的 reward_tier 完全一致：仅当展示档位为 T5（非豹子大奖）时触发「再来一次」链式免费局
        $grantsFreeTicket = ($rewardTier === 'T5');

        return [
            'player_id'          => 0,
            'admin_id'           => 0,
            'lottery_config_id'  => $configId,
            'lottery_type'       => $lotteryType,
            'is_win'             => $isWin,
            'win_coin'           => $winCoin,
            'ante'               => $ante,
            'paid_amount'        => $paidAmount,
            'super_win_coin'     => $superWinCoin,
            'reward_win_coin'    => $rewardWinCoin,
            'direction'          => $direction,
            'reward_tier'        => $rewardTier,
            'start_index'        => $startIndex,
            'target_index'       => $targetIndex,
            'roll_array'         => json_encode($rollArray),
            'roll_number'        => array_sum($rollArray),
            'status'             => self::RECORD_STATUS_SUCCESS,
            'tier'               => $tier,
            'roll_number_for_count' => $rollNumber,
            'real_ev'            => $realEv,
            'bigwin_real_ev'     => $isWin === 1 ? $bigWinRealEv : 0.0,
            'cost_ev'            => $costRealEv,
            'grants_free_ticket' => $grantsFreeTicket,
        ];
    }

    /**
     * 玩家已选彩金池配置（用于 playerDefault 运行时权重）
     */
    private function resolvePlayerLinkedPoolConfig(DicePlayer $player, int $configDeptId): ?DiceLotteryPoolConfig
    {
        $linkedId = (int) ($player->lottery_config_id ?? 0);
        if ($linkedId <= 0) {
            return null;
        }
        $cfg = DiceLotteryPoolConfig::find($linkedId);
        if (!$cfg) {
            return null;
        }
        $poolDeptId = AdminScopeHelper::normalizeRecordDeptId($cfg->dept_id ?? null);
        if ($poolDeptId !== AdminScopeHelper::normalizeRecordDeptId($configDeptId)) {
            return null;
        }
        return $cfg;
    }
}
