<?php
declare(strict_types=1);

/**
 * /functional/games/roulette.php
 * Рулетка (розважальна, без реальних грошей):
 * - 21 сектор: 0..20 (20 секторів + 1 зеро)
 * - серверний баланс фішок у БД
 * - обмін VM Coins на фішки: 1 coin = 10 chips
 * - обмін фішок на VM Coins: 15 chips = 1 coin
 * - кнопка "Поповнити" доступна тільки адміну
 * - кнопка "∞ Фішки" прибрана
 * - spin/історія працюють на сервері, а не через localStorage
 *
 * SEO: приватна зона (тільки для логіну) => noindex
 */

require_once __DIR__ . '/../../includes/start.php';

$vmCoinsLibPath = __DIR__ . '/../VMCoins/_lib.php';
if (is_file($vmCoinsLibPath)) {
    require_once $vmCoinsLibPath;
}

$gamesLibPath = __DIR__ . '/_lib.php';
if (is_file($gamesLibPath)) {
    require_once $gamesLibPath;
}

if (function_exists('require_login')) {
    require_login();
} else {
    if (!function_exists('current_user') || !current_user()) {
        header('Location: /login.php');
        exit;
    }
}

$pdo = (isset($pdo) && $pdo instanceof PDO) ? $pdo : db();
$me  = (isset($me) && is_array($me)) ? $me : current_user();

$username = is_array($me) ? (string)($me['username'] ?? 'guest') : 'guest';
$userId   = is_array($me) ? (int)($me['id'] ?? 0) : 0;

$isAdmin = false;
if (is_array($me)) {
    $role = (string)($me['role'] ?? '');
    $isAdmin = in_array($role, ['admin', 'root', 'owner'], true);
}

if (!function_exists('h')) {
    function h(?string $s): string
    {
        return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }
}

if (!function_exists('csrf_token')) {
    function csrf_token(): string
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            @session_start();
        }
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(16));
        }
        return (string)$_SESSION['csrf_token'];
    }
}

if (!function_exists('csrf_check')) {
    function csrf_check(?string $token): bool
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            @session_start();
        }
        $sess = (string)($_SESSION['csrf_token'] ?? '');
        $sent = (string)($token ?? '');
        return $sess !== '' && hash_equals($sess, $sent);
    }
}

if (!function_exists('g_lang')) {
    function g_lang(): string
    {
        $lang = (string)($_SESSION['lang'] ?? '');
        if ($lang === '' && function_exists('current_user')) {
            $u = current_user();
            $lang = (string)($u['lang'] ?? '');
        }
        if (!in_array($lang, ['uk', 'ru', 'en'], true)) {
            $lang = 'uk';
        }
        return $lang;
    }
}

if (!function_exists('gt')) {
    function gt(string $key): string
    {
        if (function_exists('t')) {
            $v = (string)t($key);
            if ($v !== '' && $v !== $key) {
                return $v;
            }
        }

        $l = g_lang();

        $dict = [
            'roulette' => ['uk' => 'Рулетка', 'ru' => 'Рулетка', 'en' => 'Roulette'],
            'games' => ['uk' => 'Ігри', 'ru' => 'Игры', 'en' => 'Games'],
            'profile' => ['uk' => 'Профіль', 'ru' => 'Профиль', 'en' => 'Profile'],
            'feed' => ['uk' => 'Стрічка', 'ru' => 'Лента', 'en' => 'Feed'],

            'chips_balance' => ['uk' => 'Баланс фішок', 'ru' => 'Баланс фишек', 'en' => 'Chips balance'],
            'coins_balance' => ['uk' => 'Баланс VM Coins', 'ru' => 'Баланс VM Coins', 'en' => 'VM Coins balance'],
            'balance' => ['uk' => 'Баланс', 'ru' => 'Баланс', 'en' => 'Balance'],
            'vm_coins' => ['uk' => 'VM Coins', 'ru' => 'VM Coins', 'en' => 'VM Coins'],

            'bet' => ['uk' => 'Ставка', 'ru' => 'Ставка', 'en' => 'Bet'],
            'spin' => ['uk' => 'Крутити', 'ru' => 'Крутить', 'en' => 'Spin'],
            'bet_type' => ['uk' => 'Тип ставки', 'ru' => 'Тип ставки', 'en' => 'Bet type'],
            'color' => ['uk' => 'Колір', 'ru' => 'Цвет', 'en' => 'Color'],
            'number' => ['uk' => 'Число', 'ru' => 'Число', 'en' => 'Number'],
            'history' => ['uk' => 'Історія', 'ru' => 'История', 'en' => 'History'],
            'result' => ['uk' => 'Результат', 'ru' => 'Результат', 'en' => 'Result'],
            'close' => ['uk' => 'Закрити', 'ru' => 'Закрыть', 'en' => 'Close'],

            'red' => ['uk' => 'Червоне', 'ru' => 'Красное', 'en' => 'Red'],
            'black' => ['uk' => 'Чорне', 'ru' => 'Чёрное', 'en' => 'Black'],
            'green' => ['uk' => 'Зелене (0)', 'ru' => 'Зелёное (0)', 'en' => 'Green (0)'],

            'enter_amount' => ['uk' => 'Вкажи суму', 'ru' => 'Укажи сумму', 'en' => 'Enter amount'],
            'not_enough' => ['uk' => 'Недостатньо фішок', 'ru' => 'Недостаточно фишек', 'en' => 'Not enough chips'],
            'not_enough_coins' => ['uk' => 'Недостатньо VM Coins', 'ru' => 'Недостаточно VM Coins', 'en' => 'Not enough VM Coins'],
            'invalid_multiple_15' => ['uk' => 'Кількість фішок має бути кратною 15', 'ru' => 'Количество фишек должно быть кратно 15', 'en' => 'Chips amount must be divisible by 15'],
            'win' => ['uk' => 'Виграш', 'ru' => 'Выигрыш', 'en' => 'Win'],
            'lose' => ['uk' => 'Програш', 'ru' => 'Проигрыш', 'en' => 'Lose'],

            'chips' => ['uk' => 'фішки', 'ru' => 'фишки', 'en' => 'chips'],
            'exchange' => ['uk' => 'Обмін', 'ru' => 'Обмен', 'en' => 'Exchange'],
            'exchange_now' => ['uk' => 'Обміняти', 'ru' => 'Обменять', 'en' => 'Exchange'],
            'coins_to_chips' => ['uk' => 'VM Coins → фішки', 'ru' => 'VM Coins → фишки', 'en' => 'VM Coins → chips'],
            'chips_to_coins' => ['uk' => 'Фішки → VM Coins', 'ru' => 'Фишки → VM Coins', 'en' => 'Chips → VM Coins'],
            'rate_coins_to_chips' => ['uk' => 'Курс: 1 VM Coin = 10 фішок', 'ru' => 'Курс: 1 VM Coin = 10 фишек', 'en' => 'Rate: 1 VM Coin = 10 chips'],
            'rate_chips_to_coins' => ['uk' => 'Курс: 15 фішок = 1 VM Coin', 'ru' => 'Курс: 15 фишек = 1 VM Coin', 'en' => 'Rate: 15 chips = 1 VM Coin'],
            'amount_coins' => ['uk' => 'Скільки VM Coins', 'ru' => 'Сколько VM Coins', 'en' => 'How many VM Coins'],
            'amount_chips' => ['uk' => 'Скільки фішок', 'ru' => 'Сколько фишек', 'en' => 'How many chips'],
            'you_get' => ['uk' => 'Отримаєш', 'ru' => 'Получишь', 'en' => 'You get'],
            'topup' => ['uk' => 'Поповнити', 'ru' => 'Пополнить', 'en' => 'Top up'],
            'topup_admin_hint' => ['uk' => 'Адмін: +1000 фішок', 'ru' => 'Админ: +1000 фишек', 'en' => 'Admin: +1000 chips'],
            'exchange_hint' => ['uk' => '1 coin = 10 chips · 15 chips = 1 coin', 'ru' => '1 coin = 10 chips · 15 chips = 1 coin', 'en' => '1 coin = 10 chips · 15 chips = 1 coin'],
            'no_spins' => ['uk' => 'Поки що немає операцій.', 'ru' => 'Пока нет операций.', 'en' => 'No operations yet.'],

            'schema_missing' => ['uk' => 'Не застосована SQL-міграція рулетки. Спочатку створи таблиці.', 'ru' => 'SQL-миграция рулетки не применена. Сначала создай таблицы.', 'en' => 'Roulette SQL migration has not been applied yet. Create the tables first.'],
            'bad_csrf' => ['uk' => 'Невірний CSRF токен', 'ru' => 'Неверный CSRF токен', 'en' => 'Invalid CSRF token'],
            'server_error' => ['uk' => 'Сталася помилка сервера', 'ru' => 'Произошла ошибка сервера', 'en' => 'Server error'],
            'invalid_request' => ['uk' => 'Некоректний запит', 'ru' => 'Некорректный запрос', 'en' => 'Invalid request'],
            'invalid_bet' => ['uk' => 'Некоректна ставка', 'ru' => 'Некорректная ставка', 'en' => 'Invalid bet'],

            'msg_exchange_to_chips' => ['uk' => 'Обмін виконано: -:coins VM Coins, +:chips фішок', 'ru' => 'Обмен выполнен: -:coins VM Coins, +:chips фишек', 'en' => 'Exchange completed: -:coins VM Coins, +:chips chips'],
            'msg_exchange_to_coins' => ['uk' => 'Обмін виконано: -:chips фішок, +:coins VM Coins', 'ru' => 'Обмен выполнен: -:chips фишек, +:coins VM Coins', 'en' => 'Exchange completed: -:chips chips, +:coins VM Coins'],
            'msg_topup_done' => ['uk' => 'Поповнення виконано: +:chips фішок', 'ru' => 'Пополнение выполнено: +:chips фишек', 'en' => 'Top up completed: +:chips chips'],
            'msg_spin_saved' => ['uk' => 'Результат збережено', 'ru' => 'Результат сохранён', 'en' => 'Result saved'],
        ];

        return $dict[$key][$l] ?? $key;
    }
}

if (!function_exists('rw_json_exit')) {
    function rw_json_exit(array $payload, int $status = 200): never
    {
        http_response_code($status);
        header('Content-Type: application/json; charset=UTF-8');
        echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }
}

if (!function_exists('rw_dt')) {
    function rw_dt(?string $dt): string
    {
        $dt = trim((string)$dt);
        if ($dt === '') {
            return '';
        }

        $ts = strtotime($dt);
        if ($ts === false) {
            return $dt;
        }

        return date('d.m H:i', $ts);
    }
}

if (!function_exists('rw_tx_history_rows')) {
    function rw_tx_history_rows(PDO $pdo, int $userId, int $limit = 10): array
    {
        if (!function_exists('games_roulette_recent_transactions')) {
            return [];
        }

        $rows = games_roulette_recent_transactions($pdo, $userId, $limit);
        $out = [];

        foreach ($rows as $tx) {
            $event = trim((string)($tx['event_key'] ?? ''));
            $delta = (int)($tx['delta_chips'] ?? 0);
            $meta  = function_exists('games_json_decode_assoc')
                ? games_json_decode_assoc((string)($tx['meta_json'] ?? ''))
                : [];

            $item = [
                'kind' => 'generic',
                'badge_text' => '•',
                'badge_color' => 'black',
                'delta' => $delta,
                'when' => rw_dt((string)($tx['created_at'] ?? '')),
                'text' => trim((string)($tx['note'] ?? '')),
            ];

            if ($event === 'spin_win' || $event === 'spin_lose') {
                $betType = (string)($meta['bet_type'] ?? '');
                $betAmount = (int)($meta['bet_amount'] ?? 0);
                $picked = '';

                if ($betType === 'color') {
                    $pickedColor = (string)($meta['bet_color'] ?? '');
                    if ($pickedColor !== '') {
                        $picked = gt($pickedColor);
                    }
                } elseif ($betType === 'number') {
                    $picked = '#' . (int)($meta['bet_number'] ?? 0);
                }

                $item['kind'] = 'spin';
                $item['badge_text'] = (string)((int)($meta['win_num'] ?? 0));
                $item['badge_color'] = (string)($meta['win_color'] ?? 'black');
                $item['text'] = (($delta >= 0) ? gt('win') : gt('lose'))
                    . ' · '
                    . gt('bet')
                    . ': '
                    . $betAmount
                    . ($picked !== '' ? (' · ' . $picked) : '');
            } elseif ($event === 'exchange_from_coins') {
                $coinsSpent = (int)($meta['coins_spent'] ?? 0);
                $chipsAdded = (int)($meta['chips_added'] ?? 0);

                $item['kind'] = 'exchange';
                $item['badge_text'] = '⇄';
                $item['badge_color'] = 'black';
                $item['text'] = '-' . $coinsSpent . ' ' . gt('vm_coins') . ' → +' . $chipsAdded . ' ' . gt('chips');
            } elseif ($event === 'exchange_to_coins') {
                $chipsSpent = (int)($meta['chips_spent'] ?? 0);
                $coinsAdded = (int)($meta['coins_added'] ?? 0);

                $item['kind'] = 'exchange';
                $item['badge_text'] = '⇄';
                $item['badge_color'] = 'black';
                $item['text'] = '-' . $chipsSpent . ' ' . gt('chips') . ' → +' . $coinsAdded . ' ' . gt('vm_coins');
            } elseif ($event === 'admin_topup') {
                $chipsAdded = (int)($meta['chips_added'] ?? 0);

                $item['kind'] = 'topup';
                $item['badge_text'] = '+';
                $item['badge_color'] = 'green';
                $item['text'] = gt('topup') . ': +' . $chipsAdded . ' ' . gt('chips');
            }

            $out[] = $item;
        }

        return $out;
    }
}

if (!function_exists('rw_state_payload')) {
    function rw_state_payload(PDO $pdo, int $userId): array
    {
        $schemaReady = function_exists('games_roulette_schema_ready')
            ? games_roulette_schema_ready($pdo)
            : false;

        $coinsBalance = function_exists('vmc_get_balance')
            ? vmc_get_balance($pdo, $userId)
            : 0;

        $chipsBalance = ($schemaReady && function_exists('games_roulette_get_balance'))
            ? games_roulette_get_balance($pdo, $userId)
            : 0;

        return [
            'schema_ready' => $schemaReady,
            'coins_balance' => $coinsBalance,
            'chips_balance' => $chipsBalance,
            'history' => $schemaReady ? rw_tx_history_rows($pdo, $userId, 10) : [],
        ];
    }
}

if (!function_exists('rw_error_message')) {
    function rw_error_message(array $res): string
    {
        $reason = trim((string)($res['reason'] ?? ''));

        return match ($reason) {
            'bad_csrf' => gt('bad_csrf'),
            'bad_action' => gt('invalid_request'),
            'bad_amount' => gt('enter_amount'),
            'bad_multiple' => gt('invalid_multiple_15'),
            'invalid_bet' => gt('invalid_bet'),
            'insufficient_chips' => gt('not_enough'),
            'insufficient_coins' => gt('not_enough_coins'),
            'schema_missing', 'vmcoins_schema_missing' => gt('schema_missing'),
            'disabled' => gt('server_error'),
            'vmcoins_lib_missing' => gt('server_error'),
            'wallet_missing' => gt('server_error'),
            'coins_tx_failed' => gt('server_error'),
            'chips_tx_failed' => gt('server_error'),
            'user_not_found' => gt('server_error'),
            'exception' => gt('server_error'),
            default => gt('server_error'),
        };
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['rw_action'])) {
    if ($userId <= 0) {
        rw_json_exit(['ok' => false, 'reason' => 'bad_user', 'message' => gt('invalid_request')], 401);
    }

    if (!csrf_check((string)($_POST['csrf'] ?? ''))) {
        rw_json_exit([
            'ok' => false,
            'reason' => 'bad_csrf',
            'message' => gt('bad_csrf'),
        ], 400);
    }

    $action = trim((string)($_POST['rw_action'] ?? ''));

    if (!function_exists('games_roulette_schema_ready') || !function_exists('games_roulette_get_balance')) {
        rw_json_exit([
            'ok' => false,
            'reason' => 'schema_missing',
            'message' => gt('schema_missing'),
        ], 500);
    }

    switch ($action) {
        case 'state':
            rw_json_exit([
                'ok' => true,
                'state' => rw_state_payload($pdo, $userId),
            ]);
            break;

        case 'exchange_to_chips':
            $coinAmount = (int)($_POST['coins'] ?? 0);

            $res = function_exists('games_roulette_exchange_coins_to_chips')
                ? games_roulette_exchange_coins_to_chips($pdo, $userId, $coinAmount)
                : ['ok' => false, 'reason' => 'server_error'];

            if (!($res['ok'] ?? false)) {
                rw_json_exit([
                    'ok' => false,
                    'reason' => (string)($res['reason'] ?? 'server_error'),
                    'message' => rw_error_message($res),
                    'state' => rw_state_payload($pdo, $userId),
                ], 400);
            }

            rw_json_exit([
                'ok' => true,
                'message' => strtr(gt('msg_exchange_to_chips'), [
                    ':coins' => (string)((int)($res['coins_spent'] ?? 0)),
                    ':chips' => (string)((int)($res['chips_added'] ?? 0)),
                ]),
                'state' => rw_state_payload($pdo, $userId),
            ]);
            break;

        case 'exchange_to_coins':
            $chipsAmount = (int)($_POST['chips'] ?? 0);

            $res = function_exists('games_roulette_exchange_chips_to_coins')
                ? games_roulette_exchange_chips_to_coins($pdo, $userId, $chipsAmount)
                : ['ok' => false, 'reason' => 'server_error'];

            if (!($res['ok'] ?? false)) {
                rw_json_exit([
                    'ok' => false,
                    'reason' => (string)($res['reason'] ?? 'server_error'),
                    'message' => rw_error_message($res),
                    'state' => rw_state_payload($pdo, $userId),
                ], 400);
            }

            rw_json_exit([
                'ok' => true,
                'message' => strtr(gt('msg_exchange_to_coins'), [
                    ':chips' => (string)((int)($res['chips_spent'] ?? 0)),
                    ':coins' => (string)((int)($res['coins_added'] ?? 0)),
                ]),
                'state' => rw_state_payload($pdo, $userId),
            ]);
            break;

        case 'topup':
            if (!$isAdmin) {
                rw_json_exit([
                    'ok' => false,
                    'reason' => 'bad_action',
                    'message' => gt('invalid_request'),
                    'state' => rw_state_payload($pdo, $userId),
                ], 403);
            }

            $res = function_exists('games_roulette_admin_topup')
                ? games_roulette_admin_topup($pdo, $userId, 1000)
                : ['ok' => false, 'reason' => 'server_error'];

            if (!($res['ok'] ?? false)) {
                rw_json_exit([
                    'ok' => false,
                    'reason' => (string)($res['reason'] ?? 'server_error'),
                    'message' => rw_error_message($res),
                    'state' => rw_state_payload($pdo, $userId),
                ], 400);
            }

            rw_json_exit([
                'ok' => true,
                'message' => strtr(gt('msg_topup_done'), [
                    ':chips' => (string)((int)($res['chips_added'] ?? 0)),
                ]),
                'state' => rw_state_payload($pdo, $userId),
            ]);
            break;

        case 'spin':
            $betType = trim((string)($_POST['bet_type'] ?? 'color'));
            $amount  = (int)($_POST['amount'] ?? 0);
            $betColor = trim((string)($_POST['bet_color'] ?? ''));
            $betNumber = (int)($_POST['bet_number'] ?? -1);

            $res = function_exists('games_roulette_spin')
                ? games_roulette_spin($pdo, $userId, [
                    'type' => $betType,
                    'amount' => $amount,
                    'color' => $betColor,
                    'number' => $betNumber,
                ])
                : ['ok' => false, 'reason' => 'server_error'];

            if (!($res['ok'] ?? false)) {
                rw_json_exit([
                    'ok' => false,
                    'reason' => (string)($res['reason'] ?? 'server_error'),
                    'message' => rw_error_message($res),
                    'state' => rw_state_payload($pdo, $userId),
                ], 400);
            }

            rw_json_exit([
                'ok' => true,
                'message' => gt('msg_spin_saved'),
                'win_index' => (int)($res['win_index'] ?? 0),
                'win_num' => (int)($res['win_num'] ?? 0),
                'win_color' => (string)($res['win_color'] ?? 'black'),
                'delta' => (int)($res['delta'] ?? 0),
                'state' => rw_state_payload($pdo, $userId),
            ]);
            break;

        default:
            rw_json_exit([
                'ok' => false,
                'reason' => 'bad_action',
                'message' => gt('invalid_request'),
            ], 400);
    }
}

$title = gt('roulette');

$profileUrl = '/profile/?u=' . rawurlencode($username);
$feedUrl    = '/';

$seo_noindex = true;

$pageState = rw_state_payload($pdo, $userId);

include __DIR__ . '/../../includes/header.php';
?>
<link rel="stylesheet" href="/css/games.css?v=13">

<div class="container-fluid py-3">
  <div class="row justify-content-center">
    <div class="col-12 col-xxl-10" style="min-width:0;">
      <div class="vm-card rw-page p-3 p-md-4">

        <div class="d-flex align-items-start justify-content-between gap-3 flex-wrap">
          <div>
            <h1 class="h4 mb-1"><?= h(gt('roulette')) ?></h1>
            <div class="text-muted small">
              <?= h((g_lang() === 'en')
                ? 'Roulette. Virtual chips and VM Coins exchange.'
                : ((g_lang() === 'ru')
                  ? 'Рулетка. Виртуальные фишки и обмен VM Coins.'
                  : 'Рулетка. Віртуальні фішки та обмін VM Coins.')) ?>
            </div>
          </div>

          <div class="d-flex gap-2 flex-wrap">
            <a class="btn btn-sm btn-outline-secondary" href="/functional/games/"><?= h(gt('games')) ?></a>
            <a class="btn btn-sm btn-outline-primary" href="<?= h($profileUrl) ?>"><?= h(gt('profile')) ?></a>
            <a class="btn btn-sm btn-outline-primary" href="<?= h($feedUrl) ?>"><?= h(gt('feed')) ?></a>
          </div>
        </div>

        <div class="rw-balance-bar mt-2">
          <div class="rw-balance-left">
            <div class="rw-balance-label"><?= h(gt('chips_balance')) ?></div>
            <div class="rw-balance-value-sm">
              <span id="rwBalance"><?= (int)($pageState['chips_balance'] ?? 0) ?></span>
              <span class="rw-balance-unit"><?= h(gt('chips')) ?></span>
            </div>
          </div>

          <div class="rw-balance-left">
            <div class="rw-balance-label"><?= h(gt('coins_balance')) ?></div>
            <div class="rw-balance-value-sm">
              <span id="rwCoinsBalance"><?= (int)($pageState['coins_balance'] ?? 0) ?></span>
              <span class="rw-balance-unit"><?= h(gt('vm_coins')) ?></span>
            </div>
          </div>

          <div class="rw-balance-right">
            <div id="rwTopupHint" class="rw-topup-hint">
              <?= h($isAdmin ? gt('topup_admin_hint') : gt('exchange_hint')) ?>
            </div>
            <?php if ($isAdmin): ?>
              <span class="badge bg-light text-dark border">Admin</span>
            <?php endif; ?>
          </div>
        </div>

        <?php if (!(bool)($pageState['schema_ready'] ?? false)): ?>
          <div class="alert alert-warning mt-3 mb-0">
            <?= h(gt('schema_missing')) ?>
          </div>
        <?php endif; ?>

        <div class="rw-bet-card mt-3">
          <div class="rw-bet-head"><?= h(gt('exchange')) ?></div>

          <div class="row g-3">
            <div class="col-lg-6">
              <div class="border rounded-3 p-3 h-100">
                <div class="fw-semibold mb-1"><?= h(gt('coins_to_chips')) ?></div>
                <div class="small text-muted mb-2"><?= h(gt('rate_coins_to_chips')) ?></div>

                <label class="form-label small text-muted mb-1"><?= h(gt('amount_coins')) ?></label>
                <div class="input-group">
                  <input id="rwCoinsToChipsAmount" type="number" min="1" step="1" class="form-control" value="1">
                  <button id="rwExchangeToChips" class="btn btn-outline-primary" type="button"><?= h(gt('exchange_now')) ?></button>
                </div>

                <div class="small text-muted mt-2">
                  <?= h(gt('you_get')) ?>:
                  <strong><span id="rwCoinsToChipsPreview">10</span> <?= h(gt('chips')) ?></strong>
                </div>
              </div>
            </div>

            <div class="col-lg-6">
              <div class="border rounded-3 p-3 h-100">
                <div class="fw-semibold mb-1"><?= h(gt('chips_to_coins')) ?></div>
                <div class="small text-muted mb-2"><?= h(gt('rate_chips_to_coins')) ?></div>

                <label class="form-label small text-muted mb-1"><?= h(gt('amount_chips')) ?></label>
                <div class="input-group">
                  <input id="rwChipsToCoinsAmount" type="number" min="15" step="15" class="form-control" value="15">
                  <button id="rwExchangeToCoins" class="btn btn-outline-primary" type="button"><?= h(gt('exchange_now')) ?></button>
                </div>

                <div class="small text-muted mt-2">
                  <?= h(gt('you_get')) ?>:
                  <strong><span id="rwChipsToCoinsPreview">1</span> <?= h(gt('vm_coins')) ?></strong>
                </div>
              </div>
            </div>
          </div>
        </div>

        <div class="rw-bet-card mt-3">
          <div class="rw-bet-head"><?= h(gt('bet')) ?></div>

          <div class="rw-bet-grid">
            <div class="rw-bet-block">
              <label class="form-label small text-muted mb-1"><?= h(gt('bet_type')) ?></label>
              <div class="rw-bet-type">
                <div class="form-check">
                  <input class="form-check-input" type="radio" name="rwType" id="rwTypeColor" value="color" checked>
                  <label class="form-check-label" for="rwTypeColor"><?= h(gt('color')) ?></label>
                </div>
                <div class="form-check">
                  <input class="form-check-input" type="radio" name="rwType" id="rwTypeNumber" value="number">
                  <label class="form-check-label" for="rwTypeNumber"><?= h(gt('number')) ?></label>
                </div>
              </div>
            </div>

            <div class="rw-bet-block" id="rwColorBox">
              <label class="form-label small text-muted mb-1"><?= h(gt('color')) ?></label>
              <select id="rwColor" class="form-select">
                <option value="red"><?= h(gt('red')) ?></option>
                <option value="black"><?= h(gt('black')) ?></option>
                <option value="green"><?= h(gt('green')) ?></option>
              </select>
            </div>

            <div class="rw-bet-block d-none" id="rwNumberBox">
              <label class="form-label small text-muted mb-1"><?= h(gt('number')) ?> (0–20)</label>
              <input id="rwNumber" type="number" min="0" max="20" class="form-control" value="7">
            </div>

            <div class="rw-bet-block rw-bet-amount">
              <label class="form-label small text-muted mb-1"><?= h(gt('bet')) ?> (<?= h(gt('chips')) ?>)</label>
              <div class="input-group">
                <input id="rwAmount" type="number" min="1" step="1" class="form-control" value="50">
                <button class="btn btn-outline-secondary" type="button" data-chip="10">+10</button>
                <button class="btn btn-outline-secondary" type="button" data-chip="50">+50</button>
                <button class="btn btn-outline-secondary" type="button" data-chip="100">+100</button>
              </div>
            </div>
          </div>
        </div>

        <div class="rw-wheel-area mt-3" id="rwWheelArea">
          <div class="rw-wheel-shell" id="rwWheelShell" aria-hidden="true">
            <div id="rwWheel" class="rw-wheel" role="img" aria-label="Roulette wheel"></div>
          </div>

          <div class="rw-inner-pointer" aria-hidden="true"></div>
          <div class="rw-center" aria-hidden="true"></div>

          <div id="rwResultModal" class="rw-result-modal" hidden>
            <div class="rw-result-box" role="dialog" aria-modal="true" aria-label="Result">
              <div class="rw-result-title"><?= h(gt('result')) ?></div>
              <div id="rwResultBody" class="rw-result-body"></div>
              <button id="rwResultClose" class="btn btn-sm btn-light border rw-result-close" type="button">
                <?= h(gt('close')) ?>
              </button>
            </div>
          </div>
        </div>

        <div class="rw-actions mt-2">
          <button id="rwSpin" class="btn btn-primary rw-btn-spin"><?= h(gt('spin')) ?></button>
          <?php if ($isAdmin): ?>
            <button id="rwTopup" class="btn btn-outline-success"><?= h(gt('topup')) ?></button>
          <?php endif; ?>
        </div>

        <div id="rwMsg" class="rw-msg mt-2" role="status" aria-live="polite"></div>

        <div class="rw-history-card mt-3">
          <div class="fw-semibold mb-2"><?= h(gt('history')) ?></div>
          <div id="rwHistory" class="rw-history"></div>
        </div>

      </div>
    </div>
  </div>
</div>

<script>
(() => {
  const USER = <?= json_encode($username, JSON_UNESCAPED_UNICODE) ?>;
  const IS_ADMIN = <?= $isAdmin ? 'true' : 'false' ?>;
  const CSRF = <?= json_encode(csrf_token(), JSON_UNESCAPED_UNICODE) ?>;
  const INITIAL_STATE = <?= json_encode($pageState, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;

  const TXT = {
    enter_amount: <?= json_encode(gt('enter_amount'), JSON_UNESCAPED_UNICODE) ?>,
    not_enough: <?= json_encode(gt('not_enough'), JSON_UNESCAPED_UNICODE) ?>,
    not_enough_coins: <?= json_encode(gt('not_enough_coins'), JSON_UNESCAPED_UNICODE) ?>,
    invalid_multiple_15: <?= json_encode(gt('invalid_multiple_15'), JSON_UNESCAPED_UNICODE) ?>,
    invalid_bet: <?= json_encode(gt('invalid_bet'), JSON_UNESCAPED_UNICODE) ?>,
    result: <?= json_encode(gt('result'), JSON_UNESCAPED_UNICODE) ?>,
    win: <?= json_encode(gt('win'), JSON_UNESCAPED_UNICODE) ?>,
    lose: <?= json_encode(gt('lose'), JSON_UNESCAPED_UNICODE) ?>,
    no_spins: <?= json_encode(gt('no_spins'), JSON_UNESCAPED_UNICODE) ?>,
    schema_missing: <?= json_encode(gt('schema_missing'), JSON_UNESCAPED_UNICODE) ?>,
    topup_admin_hint: <?= json_encode(gt('topup_admin_hint'), JSON_UNESCAPED_UNICODE) ?>,
    exchange_hint: <?= json_encode(gt('exchange_hint'), JSON_UNESCAPED_UNICODE) ?>,
    server_error: <?= json_encode(gt('server_error'), JSON_UNESCAPED_UNICODE) ?>
  };

  let state = Object.assign({
    schema_ready: false,
    coins_balance: 0,
    chips_balance: 0,
    history: []
  }, INITIAL_STATE || {});

  let spinning = false;
  let currentRotation = 0;
  let resizeTimer = null;

  const wheelAreaEl = document.getElementById('rwWheelArea');
  const wheelShellEl = document.getElementById('rwWheelShell');
  const wheelEl     = document.getElementById('rwWheel');
  const balEl       = document.getElementById('rwBalance');
  const coinsEl     = document.getElementById('rwCoinsBalance');
  const msgEl       = document.getElementById('rwMsg');
  const hisEl       = document.getElementById('rwHistory');
  const spinBtn     = document.getElementById('rwSpin');
  const topupBtn    = document.getElementById('rwTopup');
  const topupHint   = document.getElementById('rwTopupHint');

  const coinsToChipsAmountEl = document.getElementById('rwCoinsToChipsAmount');
  const chipsToCoinsAmountEl = document.getElementById('rwChipsToCoinsAmount');
  const coinsToChipsPreviewEl = document.getElementById('rwCoinsToChipsPreview');
  const chipsToCoinsPreviewEl = document.getElementById('rwChipsToCoinsPreview');
  const exchangeToChipsBtn = document.getElementById('rwExchangeToChips');
  const exchangeToCoinsBtn = document.getElementById('rwExchangeToCoins');

  const amountEl    = document.getElementById('rwAmount');
  const typeColor   = document.getElementById('rwTypeColor');
  const typeNum     = document.getElementById('rwTypeNumber');
  const colorBox    = document.getElementById('rwColorBox');
  const numBox      = document.getElementById('rwNumberBox');
  const colorEl     = document.getElementById('rwColor');
  const numberEl    = document.getElementById('rwNumber');

  const resModal = document.getElementById('rwResultModal');
  const resBody  = document.getElementById('rwResultBody');
  const resClose = document.getElementById('rwResultClose');

  const ORDER = [0,14,3,19,7,18,9,17,6,16,8,15,2,13,1,12,4,11,5,10,20];
  const SEG = 360 / ORDER.length;

  const COLOR_BY_NUM = {};
  for (let i = 0; i < ORDER.length; i++) {
    const n = ORDER[i];
    if (n === 0) COLOR_BY_NUM[n] = 'green';
    else COLOR_BY_NUM[n] = (i % 2 === 0) ? 'red' : 'black';
  }

  function escapeHtml(str) {
    return String(str ?? '')
      .replaceAll('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#039;');
  }

  function numColor(n) {
    return COLOR_BY_NUM[n] || 'black';
  }

  function clampInt(v, min, max) {
    v = Number(v);
    if (!Number.isFinite(v)) return min;
    v = Math.floor(v);
    return Math.max(min, Math.min(max, v));
  }

  function chipsBalance() {
    return Math.max(0, Number(state.chips_balance || 0));
  }

  function coinsBalance() {
    return Math.max(0, Number(state.coins_balance || 0));
  }

  function getWheelSize() {
    const shellRect = wheelShellEl ? wheelShellEl.getBoundingClientRect() : null;
    const wheelRect = wheelEl ? wheelEl.getBoundingClientRect() : null;

    const a = shellRect ? Math.round(shellRect.width) : 0;
    const b = wheelRect ? Math.round(wheelRect.width) : 0;
    const s = Math.max(a, b, 0);

    return s > 0 ? s : 420;
  }

  function syncPocketTextRotation(angle, animate = false) {
    const txts = wheelEl.querySelectorAll('.rw-pocket-txt');
    txts.forEach((txt) => {
      txt.style.transition = animate
        ? 'transform 4.2s cubic-bezier(.12,.75,.12,1)'
        : 'none';
      txt.style.transform = `rotate(${-angle}deg)`;
    });
  }

  function placePockets() {
    const ring = wheelEl.querySelector('.rw-ring');
    if (!ring) return;

    const s = getWheelSize();
    const labelSize = Math.max(18, Math.round(s * 0.062));
    const radius = Math.max(0, (s / 2) - Math.max(78, Math.floor(s * 0.20)));

    ring.querySelectorAll('.rw-pocket').forEach(el => {
      const i = Number(el.dataset.i || 0);
      const ang = (i * SEG) + (SEG / 2);
      const txt = el.querySelector('.rw-pocket-txt');

      el.style.width = `${labelSize}px`;
      el.style.height = `${Math.max(20, Math.round(labelSize * 0.78))}px`;
      el.style.marginLeft = `${-labelSize / 2}px`;
      el.style.marginTop = `${-Math.max(20, Math.round(labelSize * 0.78)) / 2}px`;
      el.style.transform = `rotate(${ang}deg) translateY(-${radius}px) rotate(${-ang}deg)`;
      el.style.visibility = 'visible';
      el.style.opacity = '1';

      if (txt) {
        txt.style.fontSize = `${Math.max(10, Math.round(labelSize * 0.34))}px`;
        txt.style.visibility = 'visible';
        txt.style.opacity = '1';
      }
    });

    syncPocketTextRotation(currentRotation, false);
  }

  function syncWheelVars() {
    const s = getWheelSize();
    wheelAreaEl.style.setProperty('--rw-size', s + 'px');
    wheelEl.style.setProperty('--rw-seg', SEG + 'deg');
    placePockets();
  }

  function scheduleWheelSync() {
    if (resizeTimer) {
      clearTimeout(resizeTimer);
    }

    requestAnimationFrame(() => {
      syncWheelVars();
      setTimeout(syncWheelVars, 40);
      setTimeout(syncWheelVars, 140);
      setTimeout(syncWheelVars, 320);
    });

    resizeTimer = setTimeout(() => {
      syncWheelVars();
    }, 500);
  }

  function buildWheel() {
    let parts = [];
    for (let i = 0; i < ORDER.length; i++) {
      const n = ORDER[i];
      const c = numColor(n);
      const start = i * SEG;
      const end = (i + 1) * SEG;
      const col = (c === 'green') ? '#1ea85f' : (c === 'red' ? '#d94747' : '#141618');
      parts.push(`${col} ${start}deg ${end}deg`);
    }

    wheelEl.style.background = `conic-gradient(from 0deg, ${parts.join(',')})`;
    wheelEl.style.position = 'relative';
    wheelEl.style.overflow = 'hidden';
    wheelEl.style.isolation = 'isolate';

    const ring = document.createElement('div');
    ring.className = 'rw-ring';
    ring.style.position = 'absolute';
    ring.style.inset = '0';
    ring.style.zIndex = '30';
    ring.style.pointerEvents = 'none';
    ring.style.transform = 'translateZ(0)';

    for (let i = 0; i < ORDER.length; i++) {
      const n = ORDER[i];
      const c = numColor(n);

      const pocket = document.createElement('div');
      pocket.className = `rw-pocket ${c}`;
      pocket.dataset.i = String(i);
      pocket.style.position = 'absolute';
      pocket.style.left = '50%';
      pocket.style.top = '50%';
      pocket.style.display = 'flex';
      pocket.style.alignItems = 'center';
      pocket.style.justifyContent = 'center';
      pocket.style.pointerEvents = 'none';
      pocket.style.zIndex = '31';
      pocket.style.overflow = 'visible';
      pocket.style.visibility = 'visible';
      pocket.style.opacity = '1';

      const txt = document.createElement('span');
      txt.className = 'rw-pocket-txt';
      txt.textContent = String(n);
      txt.style.display = 'inline-flex';
      txt.style.alignItems = 'center';
      txt.style.justifyContent = 'center';
      txt.style.width = '100%';
      txt.style.height = '100%';
      txt.style.fontWeight = '900';
      txt.style.lineHeight = '1';
      txt.style.color = '#f7d36b';
      txt.style.webkitTextStroke = '.6px rgba(80,55,0,.55)';
      txt.style.textShadow = '0 1px 0 rgba(255,244,200,.85), 0 2px 0 rgba(190,140,20,.85), 0 3px 0 rgba(140,95,0,.85), 0 6px 10px rgba(0,0,0,.45)';
      txt.style.filter = 'drop-shadow(0 2px 2px rgba(0,0,0,.35))';
      txt.style.userSelect = 'none';
      txt.style.webkitUserSelect = 'none';
      txt.style.position = 'relative';
      txt.style.zIndex = '32';
      txt.style.transform = 'rotate(0deg)';
      txt.style.transformOrigin = '50% 50%';
      txt.style.transition = 'none';
      txt.style.visibility = 'visible';
      txt.style.opacity = '1';
      txt.style.backfaceVisibility = 'hidden';
      txt.style.willChange = 'transform';

      pocket.appendChild(txt);
      ring.appendChild(pocket);
    }

    wheelEl.innerHTML = '';
    wheelEl.appendChild(ring);
    wheelEl.style.setProperty('--rw-rot', '0deg');

    syncPocketTextRotation(0, false);
    scheduleWheelSync();
  }

  function applyState(nextState) {
    if (!nextState || typeof nextState !== 'object') return;
    state = Object.assign({}, state, nextState);
    render();
  }

  function setMsg(html, type = 'info') {
    msgEl.className = `rw-msg mt-2 ${type}`;
    msgEl.innerHTML = html || '';
  }

  function renderHistory() {
    hisEl.innerHTML = '';
    const items = Array.isArray(state.history) ? state.history : [];

    if (!items.length) {
      const empty = document.createElement('div');
      empty.className = 'text-muted small';
      empty.textContent = TXT.no_spins;
      hisEl.appendChild(empty);
      return;
    }

    items.forEach(it => {
      const delta = Number(it.delta || 0);
      const row = document.createElement('div');
      row.className = 'rw-his-row';
      row.innerHTML = `
        <div class="rw-his-left">
          <span class="rw-badge ${escapeHtml(it.badge_color || 'black')}">${escapeHtml(it.badge_text || '•')}</span>
          <span class="small">${escapeHtml(it.text || '')}</span>
          <span class="small text-muted">${escapeHtml(it.when || '')}</span>
        </div>
        <div class="rw-his-right ${delta >= 0 ? 'pos' : 'neg'}">
          ${delta >= 0 ? '+' : ''}${delta}
        </div>
      `;
      hisEl.appendChild(row);
    });
  }

  function updateExchangePreviews() {
    const coins = clampInt(coinsToChipsAmountEl?.value || 1, 1, 1000000);
    const chips = clampInt(chipsToCoinsAmountEl?.value || 15, 15, 1000000000);

    if (coinsToChipsPreviewEl) {
      coinsToChipsPreviewEl.textContent = String(coins * 10);
    }

    if (chipsToCoinsPreviewEl) {
      chipsToCoinsPreviewEl.textContent = String(Math.floor(chips / 15));
    }
  }

  function render() {
    if (balEl) balEl.textContent = String(chipsBalance());
    if (coinsEl) coinsEl.textContent = String(coinsBalance());

    renderHistory();
    updateExchangePreviews();

    const schemaReady = !!state.schema_ready;

    if (!spinning && spinBtn) {
      spinBtn.disabled = !schemaReady || chipsBalance() <= 0;
    }

    if (exchangeToChipsBtn) {
      exchangeToChipsBtn.disabled = !schemaReady;
    }

    if (exchangeToCoinsBtn) {
      exchangeToCoinsBtn.disabled = !schemaReady;
    }

    if (topupBtn) {
      topupBtn.disabled = !schemaReady;
    }

    if (topupHint) {
      topupHint.textContent = IS_ADMIN ? TXT.topup_admin_hint : TXT.exchange_hint;
    }

    scheduleWheelSync();
  }

  async function request(action, payload = {}) {
    const fd = new FormData();
    fd.append('rw_action', action);
    fd.append('csrf', CSRF);

    Object.entries(payload).forEach(([k, v]) => {
      fd.append(k, String(v ?? ''));
    });

    const r = await fetch(window.location.href, {
      method: 'POST',
      body: fd,
      headers: { 'X-Requested-With': 'XMLHttpRequest' },
      credentials: 'same-origin'
    });

    let data = null;
    try {
      data = await r.json();
    } catch (e) {
      data = { ok: false, message: TXT.server_error };
    }

    return data;
  }

  function hideResult() {
    resModal.classList.remove('show');
    resModal.hidden = true;
  }

  function showResult(winNum, winCol, delta) {
    const deltaTxt = `${delta >= 0 ? '+' : ''}${delta}`;
    resBody.innerHTML = `
      <div class="rw-result-number">
        <span class="rw-badge ${escapeHtml(winCol)}">${escapeHtml(String(winNum))}</span>
      </div>
      <div class="rw-result-text">
        ${delta >= 0 ? escapeHtml(TXT.win) : escapeHtml(TXT.lose)}:
        <b class="${delta >= 0 ? 'pos' : 'neg'}">${escapeHtml(deltaTxt)}</b>
      </div>
    `;
    resModal.hidden = false;
    requestAnimationFrame(() => resModal.classList.add('show'));
  }

  resClose.addEventListener('click', hideResult);
  resModal.addEventListener('click', (e) => {
    if (e.target === resModal) hideResult();
  });

  function getBet() {
    const amount = clampInt(amountEl.value, 1, 1000000);
    const type = typeNum.checked ? 'number' : 'color';

    if (!amount || amount < 1) {
      return { ok: false, err: TXT.enter_amount };
    }

    if (chipsBalance() <= 0 || amount > chipsBalance()) {
      return { ok: false, err: TXT.not_enough };
    }

    if (type === 'color') {
      const c = String(colorEl.value || 'red');
      return { ok: true, type, amount, color: c };
    }

    const n = clampInt(numberEl.value, 0, 20);
    return { ok: true, type, amount, number: n };
  }

  function animateToIndex(winIndex, done) {
    const targetAngle = (winIndex * SEG) + (SEG / 2);
    const desiredMod = (360 - targetAngle) % 360;
    const baseMod = ((currentRotation % 360) + 360) % 360;
    const addMod = (desiredMod - baseMod + 360) % 360;
    const extraSpins = 360 * (5 + Math.floor(Math.random() * 4));
    const newRotation = currentRotation + extraSpins + addMod;

    wheelEl.style.transition = 'transform 4.2s cubic-bezier(.12,.75,.12,1)';
    wheelEl.style.transform = `rotate(${newRotation}deg)`;
    wheelEl.style.setProperty('--rw-rot', `${newRotation}deg`);

    syncPocketTextRotation(newRotation, true);

    currentRotation = newRotation;

    window.setTimeout(() => {
      syncPocketTextRotation(currentRotation, false);
      done();
      scheduleWheelSync();
    }, 4300);
  }

  async function spin() {
    if (spinning) return;

    hideResult();

    if (!state.schema_ready) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(TXT.schema_missing)}</div>`, 'warn');
      return;
    }

    const bet = getBet();
    if (!bet.ok) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(bet.err)}</div>`, 'warn');
      render();
      return;
    }

    spinning = true;
    spinBtn.disabled = true;
    setMsg('', 'info');

    const res = await request('spin', {
      bet_type: bet.type,
      amount: bet.amount,
      bet_color: bet.color ?? '',
      bet_number: bet.number ?? ''
    });

    if (!res || !res.ok) {
      spinning = false;
      render();
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(res?.message || TXT.invalid_bet)}</div>`, 'warn');
      if (res?.state) applyState(res.state);
      return;
    }

    animateToIndex(Number(res.win_index || 0), () => {
      if (res.state) applyState(res.state);
      showResult(Number(res.win_num || 0), String(res.win_color || 'black'), Number(res.delta || 0));
      spinning = false;
      render();
    });
  }

  async function exchangeToChips() {
    if (!state.schema_ready) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(TXT.schema_missing)}</div>`, 'warn');
      return;
    }

    const coins = clampInt(coinsToChipsAmountEl?.value || 1, 1, 1000000);
    if (coins < 1) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(TXT.enter_amount)}</div>`, 'warn');
      return;
    }

    if (coins > coinsBalance()) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(TXT.not_enough_coins)}</div>`, 'warn');
      return;
    }

    const res = await request('exchange_to_chips', { coins });

    if (!res || !res.ok) {
      if (res?.state) applyState(res.state);
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(res?.message || TXT.server_error)}</div>`, 'warn');
      return;
    }

    if (res.state) applyState(res.state);
    setMsg(`<div class="alert alert-success mb-0">${escapeHtml(res.message || '')}</div>`, 'ok');
  }

  async function exchangeToCoins() {
    if (!state.schema_ready) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(TXT.schema_missing)}</div>`, 'warn');
      return;
    }

    const chips = clampInt(chipsToCoinsAmountEl?.value || 15, 15, 1000000000);
    if (chips < 15) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(TXT.enter_amount)}</div>`, 'warn');
      return;
    }

    if (chips % 15 !== 0) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(TXT.invalid_multiple_15)}</div>`, 'warn');
      return;
    }

    if (chips > chipsBalance()) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(TXT.not_enough)}</div>`, 'warn');
      return;
    }

    const res = await request('exchange_to_coins', { chips });

    if (!res || !res.ok) {
      if (res?.state) applyState(res.state);
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(res?.message || TXT.server_error)}</div>`, 'warn');
      return;
    }

    if (res.state) applyState(res.state);
    setMsg(`<div class="alert alert-success mb-0">${escapeHtml(res.message || '')}</div>`, 'ok');
  }

  async function adminTopup() {
    if (!IS_ADMIN || !topupBtn) return;

    if (!state.schema_ready) {
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(TXT.schema_missing)}</div>`, 'warn');
      return;
    }

    const res = await request('topup');

    if (!res || !res.ok) {
      if (res?.state) applyState(res.state);
      setMsg(`<div class="alert alert-warning mb-0">${escapeHtml(res?.message || TXT.server_error)}</div>`, 'warn');
      return;
    }

    if (res.state) applyState(res.state);
    setMsg(`<div class="alert alert-success mb-0">${escapeHtml(res.message || '')}</div>`, 'ok');
  }

  function refreshBetUI() {
    if (typeNum.checked) {
      colorBox.classList.add('d-none');
      numBox.classList.remove('d-none');
    } else {
      numBox.classList.add('d-none');
      colorBox.classList.remove('d-none');
    }
  }

  document.querySelectorAll('[data-chip]').forEach(btn => {
    btn.addEventListener('click', () => {
      const add = Number(btn.getAttribute('data-chip') || 0);
      const cur = clampInt(amountEl.value, 1, 1000000);
      amountEl.value = String(cur + add);
    });
  });

  typeColor.addEventListener('change', refreshBetUI);
  typeNum.addEventListener('change', refreshBetUI);

  if (coinsToChipsAmountEl) {
    coinsToChipsAmountEl.addEventListener('input', updateExchangePreviews);
  }

  if (chipsToCoinsAmountEl) {
    chipsToCoinsAmountEl.addEventListener('input', updateExchangePreviews);
  }

  if (exchangeToChipsBtn) {
    exchangeToChipsBtn.addEventListener('click', exchangeToChips);
  }

  if (exchangeToCoinsBtn) {
    exchangeToCoinsBtn.addEventListener('click', exchangeToCoins);
  }

  if (spinBtn) {
    spinBtn.addEventListener('click', spin);
  }

  if (topupBtn) {
    topupBtn.addEventListener('click', adminTopup);
  }

  window.addEventListener('resize', scheduleWheelSync);
  window.addEventListener('load', scheduleWheelSync);

  if (document.fonts && document.fonts.ready) {
    document.fonts.ready.then(() => {
      scheduleWheelSync();
    }).catch(() => {});
  }

  if ('ResizeObserver' in window) {
    const ro = new ResizeObserver(() => {
      scheduleWheelSync();
    });

    if (wheelShellEl) ro.observe(wheelShellEl);
    if (wheelEl) ro.observe(wheelEl);
    if (wheelAreaEl) ro.observe(wheelAreaEl);
  }

  buildWheel();
  refreshBetUI();
  updateExchangePreviews();
  render();
  scheduleWheelSync();
})();
</script>

<?php include __DIR__ . '/../../includes/footer.php'; ?>