<?php
/**
 * BTC Catch Miner — Admin API
 * Handles all admin panel operations
 */

declare(strict_types=1);

header('Content-Type: application/json; charset=UTF-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Admin-Token');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204); exit;
}

require_once __DIR__ . '/config.php';
require_once __DIR__ . '/db.php';

// ─── ADMIN AUTH ──────────────────────────────────────────────────────────────
$adminToken = $_SERVER['HTTP_X_ADMIN_TOKEN'] ?? ($_GET['token'] ?? '');
$action     = $_GET['action'] ?? '';
$body       = json_decode(file_get_contents('php://input'), true) ?? [];

// Login action bypasses token check
if ($action !== 'login') {
    if (!verifyAdminToken($adminToken)) {
        jsonAdminError('Unauthorized.', 401);
    }
}

// ─── ROUTER ─────────────────────────────────────────────────────────────────
switch ($action) {
    // Auth
    case 'login':               handleAdminLogin($body); break;

    // Dashboard
    case 'stats':               handleStats(); break;
    case 'activity_log':        handleActivityLog(); break;

    // Users
    case 'users':               handleListUsers(); break;
    case 'user_detail':         handleUserDetail($body); break;
    case 'adjust_coins':        handleAdjustCoins($body); break;
    case 'adjust_sat':          handleAdjustSat($body); break;
    case 'ban_user':            handleBanUser($body); break;
    case 'unban_user':          handleUnbanUser($body); break;
    case 'block_withdrawal':    handleBlockWithdrawal($body); break;
    case 'unblock_withdrawal':  handleUnblockWithdrawal($body); break;
    case 'reward_user':         handleRewardUser($body); break;
    case 'reset_user':          handleResetUser($body); break;
    case 'delete_user':         handleDeleteUser($body); break;
    case 'set_level':           handleSetLevel($body); break;

    // Broadcast
    case 'broadcast':           handleBroadcast($body); break;
    case 'broadcast_status':    handleBroadcastStatus($body); break;
    case 'broadcast_history':   handleBroadcastHistory(); break;

    // Withdrawals
    case 'withdrawals':         handleWithdrawals(); break;
    case 'withdrawal_detail':   handleWithdrawalDetail($body); break;
    case 'export_users':        handleExportUsers(); break;

    // Settings
    case 'get_settings':        handleGetSettings(); break;
    case 'save_settings':       handleSaveSettings($body); break;

    default: jsonAdminError('Unknown action.', 400);
}

// ═══════════════════════════════════════════════════════════════════════════
// AUTH
// ═══════════════════════════════════════════════════════════════════════════

function verifyAdminToken(string $token): bool {
    return hash_equals(generateAdminToken(), $token);
}

function generateAdminToken(): string {
    return hash_hmac('sha256', 'admin_session_' . date('Y-m-d'), ADMIN_SECRET);
}

function handleAdminLogin(array $body): void {
    $password = $body['password'] ?? '';
    if (empty($password)) jsonAdminError('Password required.', 400);
    if (!hash_equals(ADMIN_SECRET, $password)) {
        logAdminAction('login_failed', null, ['ip' => getClientIp()]);
        jsonAdminError('Invalid password.', 401);
    }
    $token = generateAdminToken();
    logAdminAction('login_success', null, ['ip' => getClientIp()]);
    jsonAdminSuccess(['token' => $token]);
}

// ═══════════════════════════════════════════════════════════════════════════
// DASHBOARD STATS
// ═══════════════════════════════════════════════════════════════════════════

function handleStats(): void {
    $stats = [
        'total_users'           => (int)DB::val('SELECT COUNT(*) FROM players'),
        'new_today'             => (int)DB::val("SELECT COUNT(*) FROM players WHERE DATE(join_date) = CURDATE()"),
        'new_this_week'         => (int)DB::val("SELECT COUNT(*) FROM players WHERE join_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)"),
        'active_today'          => (int)DB::val("SELECT COUNT(*) FROM players WHERE DATE(last_game) = CURDATE()"),
        'active_this_week'      => (int)DB::val("SELECT COUNT(*) FROM players WHERE last_game >= DATE_SUB(NOW(), INTERVAL 7 DAY)"),
        'banned_users'          => (int)DB::val("SELECT COUNT(*) FROM players WHERE is_banned = 1"),
        'blocked_withdrawals'   => (int)DB::val("SELECT COUNT(*) FROM players WHERE withdrawal_blocked = 1"),
        'total_coins'           => (int)DB::val('SELECT COALESCE(SUM(coins), 0) FROM players'),
        'total_btc_balance'     => (int)DB::val('SELECT COALESCE(SUM(btc_balance), 0) FROM players'),
        'total_withdrawn_sat'   => (int)DB::val('SELECT COALESCE(SUM(total_withdraw), 0) FROM players'),
        'total_games_played'    => (int)DB::val('SELECT COALESCE(SUM(games_played), 0) FROM players'),
        'total_referrals'       => (int)DB::val('SELECT COUNT(*) FROM referrals'),
        'pending_withdrawals'   => (int)DB::val("SELECT COUNT(*) FROM withdrawals WHERE status = 'pending'"),
        'withdrawals_today'     => (int)DB::val("SELECT COUNT(*) FROM withdrawals WHERE DATE(created_at) = CURDATE()"),
        'sat_paid_today'        => (int)DB::val("SELECT COALESCE(SUM(amount_sat), 0) FROM withdrawals WHERE DATE(created_at) = CURDATE() AND status = 'success'"),
    ];

    // Growth chart (last 14 days)
    $growth = DB::all(
        "SELECT DATE(join_date) as date, COUNT(*) as count
         FROM players WHERE join_date >= DATE_SUB(NOW(), INTERVAL 14 DAY)
         GROUP BY DATE(join_date) ORDER BY date ASC"
    );

    // Top players
    $topPlayers = DB::all(
        "SELECT telegram_id, username, first_name, coins, btc_balance, level, games_played, is_banned
         FROM players ORDER BY coins DESC LIMIT 10"
    );

    // Recent activity
    $recentActivity = DB::all(
        "SELECT * FROM admin_logs ORDER BY created_at DESC LIMIT 20"
    );

    jsonAdminSuccess([
        'stats'           => $stats,
        'growth'          => $growth,
        'top_players'     => $topPlayers,
        'recent_activity' => $recentActivity,
    ]);
}

function handleActivityLog(): void {
    $logs = DB::all("SELECT * FROM admin_logs ORDER BY created_at DESC LIMIT 100");
    jsonAdminSuccess(['logs' => $logs]);
}

// ═══════════════════════════════════════════════════════════════════════════
// USER MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════

function handleListUsers(): void {
    $search  = sanitize($_GET['search'] ?? '');
    $filter  = $_GET['filter'] ?? 'all';
    $sort    = $_GET['sort'] ?? 'coins';
    $dir     = strtolower($_GET['dir'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC';
    $page    = max(1, (int)($_GET['page'] ?? 1));
    $limit   = 50;
    $offset  = ($page - 1) * $limit;

    $allowedSort = ['coins', 'join_date', 'last_game', 'games_played', 'level', 'btc_balance', 'total_withdraw'];
    if (!in_array($sort, $allowedSort)) $sort = 'coins';

    $where  = ['1=1'];
    $params = [];

    if ($search) {
        $where[]  = '(telegram_id LIKE ? OR username LIKE ? OR first_name LIKE ?)';
        $s        = '%' . $search . '%';
        $params   = array_merge($params, [$s, $s, $s]);
    }

    switch ($filter) {
        case 'banned':            $where[] = 'is_banned = 1'; break;
        case 'blocked':           $where[] = 'withdrawal_blocked = 1'; break;
        case 'active_today':      $where[] = 'DATE(last_game) = CURDATE()'; break;
        case 'high_balance':      $where[] = 'btc_balance >= 50'; break;
        case 'new_today':         $where[] = 'DATE(join_date) = CURDATE()'; break;
    }

    $whereStr = implode(' AND ', $where);
    $total    = (int)DB::val("SELECT COUNT(*) FROM players WHERE $whereStr", $params);
    $users    = DB::all(
        "SELECT id, telegram_id, username, first_name, photo_url, coins, btc_balance,
                total_withdraw, level, xp, games_played, best_score, game_points,
                referrals, referral_coins, join_date, last_game, last_daily,
                is_banned, withdrawal_blocked, withdrawal_block_reason, faucet_email
         FROM players WHERE $whereStr ORDER BY $sort $dir LIMIT $limit OFFSET $offset",
        $params
    );

    jsonAdminSuccess([
        'users'      => $users,
        'total'      => $total,
        'page'       => $page,
        'pages'      => (int)ceil($total / $limit),
        'limit'      => $limit,
    ]);
}

function handleUserDetail(array $body): void {
    $tid = sanitize($body['telegram_id'] ?? $_GET['telegram_id'] ?? '');
    if (!$tid) jsonAdminError('telegram_id required.', 400);

    $user = DB::row("SELECT * FROM players WHERE telegram_id = ?", [$tid]);
    if (!$user) jsonAdminError('User not found.', 404);

    $recentGames = DB::all(
        "SELECT * FROM game_sessions WHERE telegram_id = ? ORDER BY played_at DESC LIMIT 10",
        [$tid]
    );
    $recentWithdrawals = DB::all(
        "SELECT * FROM withdrawals WHERE telegram_id = ? ORDER BY created_at DESC LIMIT 10",
        [$tid]
    );
    $referralsList = DB::all(
        "SELECT p.telegram_id, p.username, p.first_name, p.coins, r.created_at
         FROM referrals r JOIN players p ON p.telegram_id = r.referred_id
         WHERE r.referrer_id = ? ORDER BY r.created_at DESC LIMIT 20",
        [$tid]
    );
    $adminActions = DB::all(
        "SELECT * FROM admin_logs WHERE target_id = ? ORDER BY created_at DESC LIMIT 20",
        [$tid]
    );

    jsonAdminSuccess([
        'user'        => $user,
        'games'       => $recentGames,
        'withdrawals' => $recentWithdrawals,
        'referrals'   => $referralsList,
        'admin_logs'  => $adminActions,
    ]);
}

function handleAdjustCoins(array $body): void {
    $tid    = sanitize($body['telegram_id'] ?? '');
    $amount = (int)($body['amount'] ?? 0);
    $reason = sanitize($body['reason'] ?? 'Admin adjustment');

    if (!$tid) jsonAdminError('telegram_id required.', 400);
    if ($amount === 0) jsonAdminError('Amount cannot be zero.', 400);

    $user = DB::row("SELECT * FROM players WHERE telegram_id = ?", [$tid]);
    if (!$user) jsonAdminError('User not found.', 404);

    if ($amount < 0 && ($user['coins'] + $amount) < 0) {
        jsonAdminError('Cannot reduce below zero.', 400);
    }

    DB::exec(
        "UPDATE players SET coins = GREATEST(0, coins + ?), btc_balance = GREATEST(0, FLOOR((coins + ?) / ?))
         WHERE telegram_id = ?",
        [$amount, $amount, COINS_PER_SAT, $tid]
    );

    logAdminAction('adjust_coins', $tid, ['amount' => $amount, 'reason' => $reason]);

    // Notify user via Telegram
    $msg = $amount > 0
        ? "🎁 Admin has added *" . number_format($amount) . " coins* to your account!\n\n💬 Reason: $reason"
        : "⚠️ Admin has removed *" . number_format(abs($amount)) . " coins* from your account.\n\n💬 Reason: $reason";
    sendTelegramMessage($tid, $msg);

    $user = DB::row("SELECT * FROM players WHERE telegram_id = ?", [$tid]);
    jsonAdminSuccess(['user' => $user]);
}

function handleAdjustSat(array $body): void {
    $tid    = sanitize($body['telegram_id'] ?? '');
    $sat    = (int)($body['sat'] ?? 0);
    $reason = sanitize($body['reason'] ?? 'Admin adjustment');

    if (!$tid) jsonAdminError('telegram_id required.', 400);
    if ($sat === 0) jsonAdminError('Amount cannot be zero.', 400);

    $user = DB::row("SELECT * FROM players WHERE telegram_id = ?", [$tid]);
    if (!$user) jsonAdminError('User not found.', 404);

    $coins = $sat * COINS_PER_SAT;

    if ($sat < 0 && ($user['btc_balance'] + $sat) < 0) {
        jsonAdminError('Cannot reduce SAT below zero.', 400);
    }

    DB::exec(
        "UPDATE players SET btc_balance = GREATEST(0, btc_balance + ?), coins = GREATEST(0, coins + ?)
         WHERE telegram_id = ?",
        [$sat, $coins, $tid]
    );

    logAdminAction('adjust_sat', $tid, ['sat' => $sat, 'coins' => $coins, 'reason' => $reason]);

    $msg = $sat > 0
        ? "🎁 Admin has added *{$sat} SAT* to your account!\n\n💬 Reason: $reason"
        : "⚠️ Admin has removed *" . abs($sat) . " SAT* from your account.\n\n💬 Reason: $reason";
    sendTelegramMessage($tid, $msg);

    $user = DB::row("SELECT * FROM players WHERE telegram_id = ?", [$tid]);
    jsonAdminSuccess(['user' => $user]);
}

function handleBanUser(array $body): void {
    $tid    = sanitize($body['telegram_id'] ?? '');
    $reason = sanitize($body['reason'] ?? 'Violation of terms');
    if (!$tid) jsonAdminError('telegram_id required.', 400);

    DB::exec("UPDATE players SET is_banned = 1 WHERE telegram_id = ?", [$tid]);
    logAdminAction('ban_user', $tid, ['reason' => $reason]);

    sendTelegramMessage($tid, "🚫 Your account has been *banned*.\n\n💬 Reason: $reason\n\nContact support if you believe this is an error.");
    jsonAdminSuccess(['message' => 'User banned.']);
}

function handleUnbanUser(array $body): void {
    $tid = sanitize($body['telegram_id'] ?? '');
    if (!$tid) jsonAdminError('telegram_id required.', 400);

    DB::exec("UPDATE players SET is_banned = 0 WHERE telegram_id = ?", [$tid]);
    logAdminAction('unban_user', $tid, []);

    sendTelegramMessage($tid, "✅ Your account has been *unbanned*. Welcome back!");
    jsonAdminSuccess(['message' => 'User unbanned.']);
}

function handleBlockWithdrawal(array $body): void {
    $tid    = sanitize($body['telegram_id'] ?? '');
    $reason = sanitize($body['reason'] ?? 'Suspicious activity');
    if (!$tid) jsonAdminError('telegram_id required.', 400);

    DB::exec("UPDATE players SET withdrawal_blocked = 1, withdrawal_block_reason = ? WHERE telegram_id = ?", [$reason, $tid]);
    logAdminAction('block_withdrawal', $tid, ['reason' => $reason]);

    sendTelegramMessage($tid, "🔒 Your withdrawal access has been temporarily *blocked*.\n\n💬 Reason: $reason\n\nContact support for assistance.");
    jsonAdminSuccess(['message' => 'Withdrawal blocked.']);
}

function handleUnblockWithdrawal(array $body): void {
    $tid = sanitize($body['telegram_id'] ?? '');
    if (!$tid) jsonAdminError('telegram_id required.', 400);

    DB::exec("UPDATE players SET withdrawal_blocked = 0, withdrawal_block_reason = NULL WHERE telegram_id = ?", [$tid]);
    logAdminAction('unblock_withdrawal', $tid, []);

    sendTelegramMessage($tid, "✅ Your withdrawal access has been *restored*. You can now withdraw again.");
    jsonAdminSuccess(['message' => 'Withdrawal unblocked.']);
}

function handleRewardUser(array $body): void {
    $tid    = sanitize($body['telegram_id'] ?? '');
    $coins  = (int)($body['coins'] ?? 0);
    $sat    = (int)($body['sat'] ?? 0);
    $reason = sanitize($body['reason'] ?? 'Admin reward');
    $message= sanitize($body['message'] ?? '');

    if (!$tid) jsonAdminError('telegram_id required.', 400);
    if ($coins <= 0 && $sat <= 0) jsonAdminError('Must reward coins or SAT.', 400);

    $user = DB::row("SELECT * FROM players WHERE telegram_id = ?", [$tid]);
    if (!$user) jsonAdminError('User not found.', 404);

    $totalCoins = $coins + ($sat * COINS_PER_SAT);
    DB::exec(
        "UPDATE players SET coins = coins + ?, btc_balance = FLOOR((coins + ?) / ?)
         WHERE telegram_id = ?",
        [$totalCoins, $totalCoins, COINS_PER_SAT, $tid]
    );

    logAdminAction('reward_user', $tid, ['coins' => $coins, 'sat' => $sat, 'reason' => $reason]);

    $rewardText = [];
    if ($coins > 0) $rewardText[] = "*" . number_format($coins) . " coins*";
    if ($sat > 0)   $rewardText[] = "*{$sat} SAT*";
    $rewardStr = implode(' + ', $rewardText);

    $customMsg = $message ? "\n\n💬 $message" : "";
    sendTelegramMessage($tid, "🏆 *Reward Received!*\n\nCongratulations! You've been rewarded {$rewardStr}!\n\n📋 Reason: $reason{$customMsg}");

    jsonAdminSuccess(['message' => 'User rewarded successfully.']);
}

function handleResetUser(array $body): void {
    $tid    = sanitize($body['telegram_id'] ?? '');
    $fields = $body['fields'] ?? [];
    if (!$tid) jsonAdminError('telegram_id required.', 400);

    $sets = [];
    if (in_array('coins', $fields))        $sets[] = "coins = 0, btc_balance = 0";
    if (in_array('games', $fields))        $sets[] = "games_played = 0, best_score = 0, xp = 0, level = 1";
    if (in_array('referrals', $fields))    $sets[] = "referrals = 0, referral_coins = 0";
    if (in_array('game_points', $fields))  $sets[] = "game_points = 10";

    if (empty($sets)) jsonAdminError('No fields to reset.', 400);

    DB::exec("UPDATE players SET " . implode(', ', $sets) . " WHERE telegram_id = ?", [$tid]);
    logAdminAction('reset_user', $tid, ['fields' => $fields]);
    jsonAdminSuccess(['message' => 'User reset.']);
}

function handleDeleteUser(array $body): void {
    $tid = sanitize($body['telegram_id'] ?? '');
    if (!$tid) jsonAdminError('telegram_id required.', 400);

    DB::exec("DELETE FROM players WHERE telegram_id = ?", [$tid]);
    DB::exec("DELETE FROM game_sessions WHERE telegram_id = ?", [$tid]);
    DB::exec("DELETE FROM withdrawals WHERE telegram_id = ?", [$tid]);
    DB::exec("DELETE FROM referrals WHERE referrer_id = ? OR referred_id = ?", [$tid, $tid]);
    logAdminAction('delete_user', $tid, []);
    jsonAdminSuccess(['message' => 'User deleted.']);
}

function handleSetLevel(array $body): void {
    $tid   = sanitize($body['telegram_id'] ?? '');
    $level = max(1, min(10, (int)($body['level'] ?? 1)));
    if (!$tid) jsonAdminError('telegram_id required.', 400);

    DB::exec("UPDATE players SET level = ? WHERE telegram_id = ?", [$level, $tid]);
    logAdminAction('set_level', $tid, ['level' => $level]);
    jsonAdminSuccess(['message' => "Level set to $level."]);
}

// ═══════════════════════════════════════════════════════════════════════════
// BROADCAST (supports 100K+ users via queue/chunk approach)
// ═══════════════════════════════════════════════════════════════════════════

function handleBroadcast(array $body): void {
    $type       = $body['type'] ?? 'text'; // text, photo, video
    $text       = trim($body['text'] ?? ''); // Do NOT htmlspecialchars — breaks HTML/Markdown parse modes
    $imageUrl   = $body['image_url'] ?? '';
    $imageBase64= $body['image_base64'] ?? '';
    $buttons    = $body['buttons'] ?? []; // [[text, url/callback]]
    $parseMode  = $body['parse_mode'] ?? 'Markdown'; // '' = plain text (omit from Telegram payload)
    $filter     = $body['filter'] ?? 'all'; // all, active, level_5_plus, etc.

    if (!$text && !$imageUrl && !$imageBase64) {
        jsonAdminError('Message text or image required.', 400);
    }

    // Get recipient list
    $where = '1=1';
    $params = [];
    switch ($filter) {
        case 'active_week': $where = "last_game >= DATE_SUB(NOW(), INTERVAL 7 DAY)"; break;
        case 'active_today': $where = "DATE(last_game) = CURDATE()"; break;
        case 'level_5_plus': $where = "level >= 5"; break;
        case 'high_balance': $where = "btc_balance >= 50"; break;
        case 'not_banned': $where = "is_banned = 0"; break;
    }
    $where .= " AND is_banned = 0"; // never broadcast to banned users

    $totalRecipients = (int)DB::val("SELECT COUNT(*) FROM players WHERE $where", $params);

    // Create broadcast job
    $broadcastId = uniqid('bc_', true);
    DB::exec(
        "INSERT INTO broadcast_jobs (broadcast_id, type, text_content, image_url, buttons_json,
         parse_mode, filter_type, total_recipients, sent_count, failed_count, status, created_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 'queued', NOW())",
        [$broadcastId, $type, $text, $imageUrl, json_encode($buttons), $parseMode, $filter, $totalRecipients]
    );

    // If image is base64, save it
    if ($imageBase64) {
        $imgData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $imageBase64));
        $imgPath = __DIR__ . '/uploads/broadcast_' . $broadcastId . '.jpg';
        if (!is_dir(__DIR__ . '/uploads')) mkdir(__DIR__ . '/uploads', 0755, true);
        file_put_contents($imgPath, $imgData);
        DB::exec("UPDATE broadcast_jobs SET image_local = ? WHERE broadcast_id = ?",
            ['uploads/broadcast_' . $broadcastId . '.jpg', $broadcastId]);
    }

    logAdminAction('broadcast_created', null, [
        'broadcast_id' => $broadcastId,
        'filter' => $filter,
        'total' => $totalRecipients,
    ]);

    // Process first chunk immediately (async via background process)
    processBroadcastChunk($broadcastId, $type, $text, $imageUrl, $buttons, $parseMode, $where, $params, $imageBase64);

    jsonAdminSuccess([
        'broadcast_id'     => $broadcastId,
        'total_recipients' => $totalRecipients,
        'status'           => 'processing',
        'message'          => "Broadcast started for $totalRecipients users.",
    ]);
}

function processBroadcastChunk(
    string $broadcastId, string $type, string $text,
    string $imageUrl, array $buttons, string $parseMode,
    string $where, array $params, string $imageBase64 = ''
): void {
    $chunkSize  = 30; // Telegram allows ~30 msg/sec
    $delayMs    = 50000; // 50ms between messages = ~20/sec (safe)
    $users      = DB::all("SELECT telegram_id FROM players WHERE $where", $params);

    // Resolve the image source: prefer local saved file (from base64 upload), then URL
    $localImgPath = '';
    if ($imageBase64) {
        $localImgPath = __DIR__ . '/uploads/broadcast_' . $broadcastId . '.jpg';
        if (!file_exists($localImgPath)) {
            // Save it now if not already saved by handleBroadcast
            $imgData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $imageBase64));
            if (!is_dir(__DIR__ . '/uploads')) mkdir(__DIR__ . '/uploads', 0755, true);
            file_put_contents($localImgPath, $imgData);
        }
    }

    // Build inline keyboard
    $replyMarkup = null;
    if (!empty($buttons)) {
        $rows = [];
        foreach ($buttons as $btn) {
            $row = ['text' => $btn['text']];
            if (!empty($btn['url']))      $row['url'] = $btn['url'];
            elseif (!empty($btn['data'])) $row['callback_data'] = $btn['data'];
            $rows[] = [$row];
        }
        $replyMarkup = json_encode(['inline_keyboard' => $rows]);
    }

    $sent   = 0;
    $failed = 0;
    $total  = count($users);

    DB::exec("UPDATE broadcast_jobs SET status = 'sending', total_recipients = ? WHERE broadcast_id = ?",
        [$total, $broadcastId]);

    foreach ($users as $i => $user) {
        $tid = $user['telegram_id'];
        $ok  = false;

        try {
            if ($type === 'photo' && ($localImgPath || $imageUrl)) {
                // Use local file path (absolute) when available — sendTelegramPhoto handles multipart
                $photoSrc = $localImgPath ?: $imageUrl;
                $ok = sendTelegramPhoto($tid, $photoSrc, $text, $parseMode, $replyMarkup);
            } else {
                $ok = sendTelegramMessage($tid, $text, $parseMode, $replyMarkup);
            }
        } catch (Exception $e) {
            $ok = false;
        }

        if ($ok) $sent++; else $failed++;

        // Update progress every 100 users
        if ($i % 100 === 0) {
            DB::exec(
                "UPDATE broadcast_jobs SET sent_count = ?, failed_count = ? WHERE broadcast_id = ?",
                [$sent, $failed, $broadcastId]
            );
        }

        usleep($delayMs); // Rate limit compliance
    }

    DB::exec(
        "UPDATE broadcast_jobs SET sent_count = ?, failed_count = ?, status = 'completed', completed_at = NOW()
         WHERE broadcast_id = ?",
        [$sent, $failed, $broadcastId]
    );

    logAdminAction('broadcast_completed', null, [
        'broadcast_id' => $broadcastId,
        'sent'         => $sent,
        'failed'       => $failed,
    ]);
}

function handleBroadcastStatus(array $body): void {
    $id = sanitize($body['broadcast_id'] ?? $_GET['broadcast_id'] ?? '');
    if (!$id) jsonAdminError('broadcast_id required.', 400);

    $job = DB::row("SELECT * FROM broadcast_jobs WHERE broadcast_id = ?", [$id]);
    if (!$job) jsonAdminError('Broadcast not found.', 404);

    $progress = $job['total_recipients'] > 0
        ? round(($job['sent_count'] + $job['failed_count']) / $job['total_recipients'] * 100, 1)
        : 0;

    jsonAdminSuccess(['job' => $job, 'progress' => $progress]);
}

function handleBroadcastHistory(): void {
    $jobs = DB::all("SELECT * FROM broadcast_jobs ORDER BY created_at DESC LIMIT 50");
    jsonAdminSuccess(['jobs' => $jobs]);
}

// ═══════════════════════════════════════════════════════════════════════════
// WITHDRAWALS
// ═══════════════════════════════════════════════════════════════════════════

function handleWithdrawals(): void {
    $status = $_GET['status'] ?? 'all';
    $page   = max(1, (int)($_GET['page'] ?? 1));
    $limit  = 50;
    $offset = ($page - 1) * $limit;

    $where  = '1=1';
    if (in_array($status, ['pending','processing','success','failed'])) {
        $where = "w.status = '$status'";
    }

    $total = (int)DB::val("SELECT COUNT(*) FROM withdrawals w WHERE $where");
    $rows  = DB::all(
        "SELECT w.*, p.username, p.first_name, p.level, p.is_banned
         FROM withdrawals w LEFT JOIN players p ON p.telegram_id = w.telegram_id
         WHERE $where ORDER BY w.created_at DESC LIMIT $limit OFFSET $offset"
    );

    jsonAdminSuccess(['withdrawals' => $rows, 'total' => $total, 'page' => $page, 'pages' => (int)ceil($total / $limit)]);
}

function handleWithdrawalDetail(array $body): void {
    $id = sanitize($body['withdrawal_id'] ?? $_GET['withdrawal_id'] ?? '');
    if (!$id) jsonAdminError('withdrawal_id required.', 400);

    $w = DB::row("SELECT * FROM withdrawals WHERE withdrawal_id = ?", [$id]);
    if (!$w) jsonAdminError('Not found.', 404);

    jsonAdminSuccess(['withdrawal' => $w]);
}

// ═══════════════════════════════════════════════════════════════════════════
// EXPORT
// ═══════════════════════════════════════════════════════════════════════════

function handleExportUsers(): void {
    header('Content-Type: text/csv; charset=UTF-8');
    header('Content-Disposition: attachment; filename="players_export_' . date('Y-m-d') . '.csv"');

    $users = DB::all("SELECT telegram_id, username, first_name, faucet_email, coins, btc_balance,
        total_withdraw, level, xp, games_played, best_score, referrals, join_date, last_game,
        is_banned, withdrawal_blocked, withdrawal_block_reason FROM players ORDER BY coins DESC");

    echo "\xEF\xBB\xBF"; // BOM for Excel
    echo "Telegram ID,Username,First Name,Email,Coins,BTC Balance (SAT),Total Withdrawn (SAT),Level,XP,Games Played,Best Score,Referrals,Join Date,Last Game,Banned,Withdrawal Blocked\n";

    foreach ($users as $u) {
        echo implode(',', array_map(fn($v) => '"' . str_replace('"', '""', (string)$v) . '"', [
            $u['telegram_id'], $u['username'], $u['first_name'], $u['faucet_email'],
            $u['coins'], $u['btc_balance'], $u['total_withdraw'], $u['level'], $u['xp'],
            $u['games_played'], $u['best_score'], $u['referrals'],
            $u['join_date'], $u['last_game'], $u['is_banned'] ? 'Yes' : 'No',
            $u['withdrawal_blocked'] ? 'Yes' : 'No',
        ])) . "\n";
    }
    exit;
}

// ═══════════════════════════════════════════════════════════════════════════
// SETTINGS
// ═══════════════════════════════════════════════════════════════════════════

function handleGetSettings(): void {
    $settings = DB::all("SELECT setting_key, setting_value FROM admin_settings");
    $map = [];
    foreach ($settings as $s) $map[$s['setting_key']] = $s['setting_value'];

    // Also return current config constants
    $map['coins_per_sat']       = COINS_PER_SAT;
    $map['min_withdraw_sat']    = MIN_WITHDRAW_SAT;
    $map['max_withdraw_sat']    = MAX_WITHDRAW_SAT;
    $map['daily_bonus_coins']   = DAILY_BONUS_COINS;
    $map['referral_bonus']      = REFERRAL_BONUS_COINS;
    $map['game_duration']       = GAME_DURATION_SEC;
    $map['game_cooldown']       = GAME_COOLDOWN_SEC;
    $map['max_score_per_game']  = MAX_SCORE_PER_GAME;

    jsonAdminSuccess(['settings' => $map]);
}

function handleSaveSettings(array $body): void {
    $allowed = ['maintenance_mode', 'registration_enabled', 'withdrawal_enabled',
                'announcement', 'min_withdraw_override', 'max_withdraw_override'];

    foreach ($body as $key => $value) {
        if (!in_array($key, $allowed)) continue;
        DB::exec(
            "INSERT INTO admin_settings (setting_key, setting_value) VALUES (?, ?)
             ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)",
            [$key, (string)$value]
        );
    }
    logAdminAction('save_settings', null, array_intersect_key($body, array_flip($allowed)));
    jsonAdminSuccess(['message' => 'Settings saved.']);
}

// ═══════════════════════════════════════════════════════════════════════════
// TELEGRAM HELPERS
// ═══════════════════════════════════════════════════════════════════════════

function sendTelegramMessage(string $chatId, string $text, string $parseMode = 'Markdown', ?string $replyMarkup = null): bool {
    $params = [
        'chat_id' => $chatId,
        'text'    => $text,
    ];
    if ($parseMode !== '') $params['parse_mode'] = $parseMode; // omit for plain text
    if ($replyMarkup)      $params['reply_markup'] = $replyMarkup;

    $ch = curl_init('https://api.telegram.org/bot' . BOT_TOKEN . '/sendMessage');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($params),
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_TIMEOUT        => 10,
    ]);
    $res = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($res, true);
    return !empty($data['ok']);
}

function sendTelegramPhoto(string $chatId, string $photo, string $caption = '', string $parseMode = 'Markdown', ?string $replyMarkup = null): bool {
    // Detect if $photo is a local file path (for base64-uploaded images)
    $isLocalFile = (strpos($photo, '/') === 0 || strpos($photo, './') === 0) && file_exists($photo);

    if ($isLocalFile) {
        // Use multipart/form-data so Telegram can receive the raw file bytes
        $params = [
            'chat_id' => $chatId,
            'photo'   => new CURLFile($photo, 'image/jpeg', 'photo.jpg'),
        ];
        if ($caption !== '')  $params['caption']     = $caption;
        if ($parseMode !== '') $params['parse_mode']  = $parseMode;
        if ($replyMarkup)      $params['reply_markup'] = $replyMarkup;

        $ch = curl_init('https://api.telegram.org/bot' . BOT_TOKEN . '/sendPhoto');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $params,   // multipart — no Content-Type header needed
            CURLOPT_TIMEOUT        => 30,
        ]);
    } else {
        // URL or Telegram file_id — send as JSON
        $params = [
            'chat_id' => $chatId,
            'photo'   => $photo,
        ];
        if ($caption !== '')   $params['caption']      = $caption;
        if ($parseMode !== '')  $params['parse_mode']   = $parseMode;
        if ($replyMarkup)       $params['reply_markup'] = $replyMarkup;

        $ch = curl_init('https://api.telegram.org/bot' . BOT_TOKEN . '/sendPhoto');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($params),
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
            CURLOPT_TIMEOUT        => 30,
        ]);
    }

    $res  = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($res, true);
    return !empty($data['ok']);
}

// ═══════════════════════════════════════════════════════════════════════════
// LOGGING
// ═══════════════════════════════════════════════════════════════════════════

function logAdminAction(string $action, ?string $targetId, array $data): void {
    try {
        DB::exec(
            "INSERT INTO admin_logs (action, target_id, data_json, ip, created_at) VALUES (?, ?, ?, ?, NOW())",
            [$action, $targetId, json_encode($data), getClientIp()]
        );
    } catch (Exception $e) {
        // Non-fatal
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════════════════════

function sanitize(string $s): string {
    return htmlspecialchars(trim($s), ENT_QUOTES, 'UTF-8');
}

function getClientIp(): string {
    return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0')[0];
}

function jsonAdminSuccess(array $data): void {
    echo json_encode(['ok' => true, ...$data]);
    exit;
}

function jsonAdminError(string $message, int $code = 400): void {
    http_response_code($code);
    echo json_encode(['ok' => false, 'error' => $message]);
    exit;
}