<?php

declare(strict_types=1);

namespace app\api\controller;

use app\common\library\game\BetChips;
use app\common\library\game\StreakWinReward;
use app\common\library\game\ZiHuaDictionary;
use app\common\model\BetOrder;
use app\common\model\GameRecord;
use app\common\model\UserWalletRecord;
use app\common\service\GameHotDataCoordinator;
use app\common\service\GameHotDataRedis;
use app\common\service\GameRecordService;
use app\common\service\GameWebSocketEventBus;
use app\common\service\GameWebSocketPayloadHelper;
use support\think\Db;
use Webman\Http\Request;
use support\Response;

class Game extends MobileBase
{
    protected array $noNeedLogin = ['dictionaryList'];

    public function lobbyInit(Request $request): Response
    {
        $response = $this->initializeMobile($request);
        if ($response !== null) {
            return $response;
        }

        $periodRow = GameHotDataRedis::gameRecordLatest();
        $now = time();
        $startAt = $periodRow ? $this->intValue($periodRow['period_start_at'] ?? 0) : $now;
        $lockAt = $startAt + 20;
        $openAt = $startAt + 22;
        $countdown = $periodRow ? max(0, ($startAt + 30) - $now) : 0;

        $dictionaryConfig = GameHotDataRedis::gameConfigRow(ZiHuaDictionary::CONFIG_KEY) ?? [];
        $dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig['config_value'] ?? null);
        $items = [];
        foreach ($dictionaryItems as $row) {
            $items[] = [
                'number' => $row['no'],
                'name' => $row['name'],
                'category' => $row['category'],
                'icon' => '',
            ];
        }

        $user = $this->auth->getUser();
        $currentStreakRaw = $user->current_streak ?? 0;
        $currentStreakParsed = filter_var($currentStreakRaw, FILTER_VALIDATE_INT);
        $currentStreak = $currentStreakParsed === false ? 0 : $currentStreakParsed;
        return $this->mobileSuccess([
            'server_time' => $now,
            'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
            'period' => [
                'period_no' => (string) ($periodRow['period_no'] ?? ''),
                'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
                'countdown' => $countdown,
                'lock_at' => $lockAt,
                'open_at' => $openAt,
            ],
            'bet_config' => array_merge(
                [
                    'pick_max_number_count' => $this->getPickMaxNumberCount(),
                    'min_bet_per_number' => $this->getConfigValue('min_bet_per_number', '0.0100'),
                    'max_bet_per_number' => $this->getConfigValue('max_bet_per_number', '10000.0000'),
                ],
                BetChips::lobbyChipsPayload()
            ),
            'dictionary' => $items,
            'user_snapshot' => array_merge(
                [
                    'coin' => $user->coin,
                ],
                StreakWinReward::playerBetOddsForCurrentStreak($currentStreak)
            ),
        ]);
    }

    public function dictionaryList(Request $request): Response
    {
        $response = $this->initializeMobile($request);
        if ($response !== null) {
            return $response;
        }
        $dictionaryConfig = GameHotDataRedis::gameConfigRow(ZiHuaDictionary::CONFIG_KEY) ?? [];
        $dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig['config_value'] ?? null);
        $items = [];
        foreach ($dictionaryItems as $row) {
            $items[] = [
                'number' => $row['no'],
                'name' => $row['name'],
                'category' => $row['category'],
                'icon' => '',
            ];
        }
        return $this->mobileSuccess([
            'version' => (string) ($dictionaryConfig['update_time'] ?? '1'),
            'items' => $items,
        ]);
    }

    public function periodCurrent(Request $request): Response
    {
        $response = $this->initializeMobile($request);
        if ($response !== null) {
            return $response;
        }
        $periodRow = GameHotDataRedis::gameRecordLatest();
        if (!$periodRow) {
            return $this->mobileError(2002, 'Game period does not exist');
        }
        $now = time();
        $startAt = $this->intValue($periodRow['period_start_at'] ?? 0);
        return $this->mobileSuccess([
            'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
            'period_id' => $periodRow['id'],
            'period_no' => $periodRow['period_no'],
            'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
            'countdown' => max(0, ($startAt + 30) - $now),
            'bet_close_in' => max(0, ($startAt + 20) - $now),
            'result_number' => $periodRow['result_number'] ?? null,
        ]);
    }

    /**
     * 兼容旧路由：/api/game/betPlace
     * 与 placeBet 一致：须传筹码标识 bet_id（1–6），单注金额取自后台 game_config 的 bet_chips。
     */
    public function betPlace(Request $request): Response
    {
        return $this->placeBet($request);
    }

    /**
     * 提交下注：入参为 period_no + numbers + bet_id（1–6，对应 lobbyInit.bet_config.chips 的键）+ idempotency_key。
     */
    public function placeBet(Request $request): Response
    {
        $response = $this->initializeMobile($request);
        if ($response !== null) {
            return $response;
        }
        $periodNo = trim((string) $request->post('period_no', ''));
        $numbersRaw = $request->post('numbers', '');
        $idempotencyKey = trim((string) $request->post('idempotency_key', ''));
        $chipPick = $this->resolveBetChipFromRequest($request);
        if (isset($chipPick['error'])) {
            return $chipPick['error'];
        }
        $betChipId = $chipPick['bet_id'];
        $singleBetAmount = $chipPick['amount'];
        if ($periodNo === '' || $idempotencyKey === '') {
            return $this->mobileError(1001, 'Missing parameters');
        }
        if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
            return $this->mobileError(1003, 'Invalid parameter value');
        }
        $numbers = $this->parseBetNumbersFromRequest($numbersRaw);
        if ($numbers === []) {
            return $this->mobileError(1003, 'Invalid parameter value');
        }
        $maxSelect = $this->getPickMaxNumberCount();
        if (count($numbers) > $maxSelect) {
            return $this->mobileError(1003, 'Invalid parameter value');
        }
        $singleAmount = bcadd($singleBetAmount, '0', 2);
        $minPer = trim((string) $this->getConfigValue('min_bet_per_number', '0.0100'));
        $maxPer = trim((string) $this->getConfigValue('max_bet_per_number', '10000.0000'));
        if (!is_numeric($minPer) || bccomp($minPer, '0', 4) <= 0) {
            $minPer = '0.0100';
        }
        if (!is_numeric($maxPer) || bccomp($maxPer, $minPer, 4) < 0) {
            $maxPer = '10000.0000';
        }
        if (bccomp($singleAmount, $minPer, 4) < 0 || bccomp($singleAmount, $maxPer, 4) > 0) {
            return $this->mobileError(1003, 'Bet amount out of range');
        }
        $numberCount = (string) count($numbers);
        $totalAmount = bcmul($singleAmount, $numberCount, 2);

        if (!GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE)) {
            return $this->mobileError(3001, 'Game is paused');
        }
        $period = GameRecord::where('period_no', $periodNo)->find();
        if (!$period) {
            return $this->mobileError(2002, 'Game period does not exist');
        }
        if ($this->intValue($period->status) !== 0) {
            return $this->mobileError(3002, 'Betting is closed');
        }

        $userIdRaw = $this->auth->id ?? null;
        $userId = filter_var($userIdRaw, FILTER_VALIDATE_INT);
        if ($userId === false || $userId <= 0) {
            return $this->mobileError(1001, 'Missing parameters');
        }

        $user = $this->auth->getUser();
        if (bccomp((string) $user->coin, $totalAmount, 2) < 0) {
            return $this->mobileError(2001, 'Insufficient balance');
        }

        $exists = BetOrder::where('idempotency_key', $idempotencyKey)->find();
        if ($exists) {
            return $this->mobileError(3003, 'Duplicate request');
        }

        $lock = GameHotDataRedis::userAdminMutationLockTry($userId);
        if (!$lock['acquired']) {
            return $this->mobileError(5000, (string) __('This user is being operated by another admin (wallet/concurrent save); please try again later'));
        }

        try {
            $coinRow = Db::name('user')->where('id', $userId)->field(['coin', 'channel_id', 'current_streak'])->find();
            if (!$coinRow) {
                return $this->mobileError(5000, 'System is busy, please try again later');
            }
            $before = (string) ($coinRow['coin'] ?? '0');
            if (bccomp($before, $totalAmount, 2) < 0) {
                return $this->mobileError(2001, 'Insufficient balance');
            }

            $existsLocked = BetOrder::where('idempotency_key', $idempotencyKey)->find();
            if ($existsLocked) {
                return $this->mobileError(3003, 'Duplicate request');
            }

            $channelIdRaw = $coinRow['channel_id'] ?? null;
            $channelId = filter_var($channelIdRaw, FILTER_VALIDATE_INT);
            if ($channelId === false) {
                $channelId = null;
            }

            $now = time();
            $after = bcsub($before, $totalAmount, 2);
            $orderNo = 'BO' . date('YmdHis') . substr(str_replace('.', '', uniqid('', true)), -6);
            $streakAtBet = (int) ($coinRow['current_streak'] ?? 0);

            Db::startTrans();
            try {
                $affected = Db::name('user')->where('id', $userId)->where('coin', $before)->update([
                    'coin' => $after,
                    'update_time' => $now,
                ]);
                if ($affected !== 1) {
                    Db::rollback();
                    return $this->mobileError(5000, (string) __('Debit failed: user balance changed by another request; please refresh and retry'));
                }

                UserWalletRecord::create([
                    'user_id' => $userId,
                    'channel_id' => $channelId,
                    'biz_type' => 'bet',
                    'direction' => 2,
                    'amount' => $totalAmount,
                    'balance_before' => $before,
                    'balance_after' => $after,
                    'ref_type' => 'bet_order',
                    'remark' => '移动端下注',
                    'create_time' => $now,
                ]);
                BetOrder::create([
                    'period_id' => $period->id,
                    'period_no' => $period->period_no,
                    'user_id' => $userId,
                    'channel_id' => $channelId,
                    'pick_numbers' => $numbers,
                    'total_amount' => $totalAmount,
                    'streak_at_bet' => $streakAtBet,
                    'is_auto' => 0,
                    'status' => 1,
                    'idempotency_key' => $idempotencyKey,
                    'create_time' => $now,
                    'update_time' => $now,
                ]);
                Db::commit();
            } catch (\Throwable $e) {
                Db::rollback();
                return $this->mobileError(5000, 'System is busy, please try again later', ['detail' => $e->getMessage()]);
            }

            GameHotDataCoordinator::afterUserCommitted($userId);
            GameWebSocketEventBus::publish('bet.accepted', GameWebSocketPayloadHelper::mergeUserStreakInto([
                'user_id' => $userId,
                'period_no' => $period->period_no,
                'numbers' => $numbers,
                'bet_id' => $betChipId,
                'single_bet_amount' => $singleAmount,
                'numbers_count' => count($numbers),
                'total_amount' => $totalAmount,
                'balance_after' => $after,
                'accepted_at' => time(),
            ], $userId, $streakAtBet));
            GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto([
                'user_id' => $userId,
                'balance_after' => $after,
                'biz_type' => 'bet',
                'changed_at' => time(),
            ], $userId, $streakAtBet));
            return $this->mobileSuccess([
                'order_no' => $orderNo,
                'period_no' => $period->period_no,
                'status' => 'accepted',
                'bet_id' => $betChipId,
                'single_bet_amount' => $singleAmount,
                'numbers_count' => count($numbers),
                'locked_balance' => '0.00',
                'balance_after' => $after,
                'current_streak' => $streakAtBet,
            ]);
        } finally {
            GameHotDataRedis::userAdminMutationLockRelease($userId, $lock['token'], $lock['redis_lock']);
        }
    }

    /**
     * 自动托管（无推送模式）：先落托管配置，实际执行仍由客户端轮询 current_status 驱动。
     */
    public function autoSpin(Request $request): Response
    {
        $response = $this->initializeMobile($request);
        if ($response !== null) {
            return $response;
        }
        $action = trim((string) $request->post('action', 'start'));
        if ($action === 'stop') {
            return $this->mobileSuccess([
                'status' => 'stopped',
                'auto_mode' => false,
            ]);
        }

        $periodNo = trim((string) $request->post('period_no', ''));
        $numbersRaw = $request->post('numbers', '');
        $rounds = $this->intValue($request->post('rounds', 1));
        $chipPick = $this->resolveBetChipFromRequest($request);
        if (isset($chipPick['error'])) {
            return $chipPick['error'];
        }
        $singleBetAmount = $chipPick['amount'];
        if ($periodNo === '' || $rounds < 1) {
            return $this->mobileError(1001, 'Missing parameters');
        }
        if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
            return $this->mobileError(1003, 'Invalid parameter value');
        }
        $numbers = $this->parseBetNumbersFromRequest($numbersRaw);
        if ($numbers === []) {
            return $this->mobileError(1003, 'Invalid parameter value');
        }
        $userIdValue = filter_var($this->auth->id ?? null, FILTER_VALIDATE_INT);
        if ($userIdValue === false) {
            $userIdValue = 0;
        }
        GameWebSocketEventBus::publish('auto.spin.progress', [
            'user_id' => $userIdValue,
            'period_no' => $periodNo,
            'bet_id' => $chipPick['bet_id'],
            'rounds' => $rounds,
            'remaining_rounds' => $rounds,
            'completed_rounds' => 0,
            'status' => 'scheduled',
            'server_time' => time(),
        ]);
        return $this->mobileSuccess([
            'status' => 'scheduled',
            'auto_mode' => true,
            'period_no' => $periodNo,
            'numbers' => $numbers,
            'bet_id' => $chipPick['bet_id'],
            'single_bet_amount' => bcadd($singleBetAmount, '0', 2),
            'rounds' => $rounds,
            'remaining_rounds' => $rounds,
        ]);
    }

    public function betMyOrders(Request $request): Response
    {
        $response = $this->initializeMobile($request);
        if ($response !== null) {
            return $response;
        }
        $page = $this->intValue($request->input('page', 1));
        $pageSize = $this->intValue($request->input('page_size', 20));
        $paginate = BetOrder::where('user_id', $this->auth->id)->order('id', 'desc')->paginate([
            'page' => $page,
            'list_rows' => $pageSize,
        ]);

        $rows = [];
        foreach ($paginate->items() as $item) {
            $rows[] = [
                'order_no' => (string) $item->id,
                'period_no' => $item->period_no,
                'numbers' => $item->pick_numbers ?? [],
                // 整笔压注金额（本笔总扣款）
                'bet_amount' => $item->total_amount,
                'total_amount' => $item->total_amount,
                'result_number' => null,
                'win_amount' => $item->win_amount,
                'status' => (string) $item->status,
                'create_time' => $item->create_time,
            ];
        }

        return $this->mobileSuccess([
            'list' => $rows,
            'pagination' => [
                'page' => $paginate->currentPage(),
                'page_size' => $paginate->listRows(),
                'total' => $paginate->total(),
            ],
        ]);
    }

    /**
     * 下注号码：`numbers` 为逗号分隔字符串（如 `1,8,16`）；兼容旧版 JSON 数组。
     *
     * @return list<int>
     */
    private function parseBetNumbersFromRequest($numbersRaw): array
    {
        if (is_array($numbersRaw)) {
            $out = [];
            foreach ($numbersRaw as $v) {
                $n = filter_var($v, FILTER_VALIDATE_INT);
                if ($n === false || $n < 1 || $n > 36) {
                    return [];
                }
                $out[] = $n;
            }
            $out = array_values(array_unique($out));
            sort($out);

            return $out;
        }
        $raw = trim((string) $numbersRaw);
        if ($raw === '') {
            return [];
        }
        $parts = preg_split('/\s*,\s*/', $raw);
        $out = [];
        foreach ($parts as $p) {
            if ($p === '') {
                continue;
            }
            $n = filter_var($p, FILTER_VALIDATE_INT);
            if ($n === false || $n < 1 || $n > 36) {
                return [];
            }
            $out[] = $n;
        }
        $out = array_values(array_unique($out));
        sort($out);

        return $out;
    }

    private function mapPeriodStatus($status): string
    {
        if ($this->intValue($status) === 0) {
            return 'betting';
        }
        if ($this->intValue($status) === 1) {
            return 'locked';
        }
        if ($this->intValue($status) === 2 || $this->intValue($status) === 3) {
            return 'settling';
        }
        if ($this->intValue($status) === 5) {
            return 'void';
        }
        return 'finished';
    }

    /**
     * @return array{bet_id: int, amount: string}|array{error: Response}
     */
    private function resolveBetChipFromRequest(Request $request): array
    {
        $betIdRaw = $request->post('bet_id', '');
        if ($betIdRaw === '' || $betIdRaw === null) {
            return ['error' => $this->mobileError(1001, 'Missing parameters')];
        }
        $betId = filter_var($betIdRaw, FILTER_VALIDATE_INT);
        if ($betId === false || $betId < 1 || $betId > 6) {
            return ['error' => $this->mobileError(1003, 'Invalid parameter value')];
        }
        $resolved = BetChips::resolveFromHotData();
        $amount = BetChips::amountForBetId($betId, $resolved['map']);
        if ($amount === null) {
            return ['error' => $this->mobileError(1003, 'Invalid parameter value')];
        }

        return ['bet_id' => $betId, 'amount' => $amount];
    }

    /**
     * 单注最多可选号码个数：`game_config.config_key = pick_max_number_count`
     */
    private function getPickMaxNumberCount(): int
    {
        $v = $this->intValue($this->getConfigValue('pick_max_number_count', '10'));
        if ($v < 1) {
            return 1;
        }
        if ($v > 36) {
            return 36;
        }

        return $v;
    }

    private function getConfigValue(string $key, string $default): string
    {
        $row = GameHotDataRedis::gameConfigRow($key);
        if ($row === null) {
            return $default;
        }
        $value = $row['config_value'] ?? null;
        if ($value === null || $value === '') {
            return $default;
        }
        return (string) $value;
    }

    private function intValue($value): int
    {
        $result = filter_var($value, FILTER_VALIDATE_INT);
        if ($result === false) {
            return 0;
        }
        return $result;
    }
}

