<?php
/**
 * BTC Catch Miner — API Handler (MySQL Edition)
 */

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-Telegram-Init-Data');

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

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

// ─── INPUT ──────────────────────────────────────────────────────────────────
$action = $_GET['action'] ?? '';
$body   = json_decode(file_get_contents('php://input'), true) ?? [];

// ─── RATE LIMITING ──────────────────────────────────────────────────────────
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$ip = explode(',', $ip)[0];
if (!checkRateLimit($ip)) {
    jsonError('Rate limit exceeded. Slow down.', 429);
}

// ─── VALIDATE TELEGRAM INIT DATA ────────────────────────────────────────────
$initData = $_SERVER['HTTP_X_TELEGRAM_INIT_DATA'] ?? $body['initData'] ?? '';
$tgUser   = null;

if ($initData && $action !== 'config') {
    $tgUser = validateTelegramInitData($initData);
    if (!$tgUser) {
        jsonError('Invalid Telegram authentication.', 401);
    }
}

// ─── ROUTER ─────────────────────────────────────────────────────────────────
switch ($action) {
    case 'config':              jsonSuccess(getConfig()); break;
    case 'login':               handleLogin($body, $tgUser); break;
    case 'submit_score':        handleSubmitScore($body, $tgUser); break;
    case 'daily_bonus':         handleDailyBonus($tgUser); break;
    case 'leaderboard':         handleLeaderboard(); break;
    case 'withdraw':            handleWithdraw($body, $tgUser); break;
    case 'get_player':          handleGetPlayer($tgUser); break;
    case 'withdrawals':         handleWithdrawalHistory($tgUser); break;
    case 'global_withdrawals':  handleGlobalWithdrawals(); break;
    case 'withdraw_status':     handleWithdrawStatus($body, $tgUser); break;
    case 'task_reward':         handleTaskReward($body, $tgUser); break;
    case 'verify_channel_join': handleVerifyChannelJoin($tgUser); break;
    case 'save_faucet_email':   handleSaveFaucetEmail($body, $tgUser); break;
    default:                    jsonError('Unknown action.', 400);
}

// ═══════════════════════════════════════════════════════════════════════════
// HANDLERS
// ═══════════════════════════════════════════════════════════════════════════

function handleLogin(array $body, ?array $tgUser): void {
    $tid       = (string)($tgUser['id'] ?? $body['telegram_id'] ?? '');
    $username  = sanitize($tgUser['username'] ?? $tgUser['first_name'] ?? $body['username'] ?? 'Player');
    $firstName = sanitize($tgUser['first_name'] ?? $body['first_name'] ?? '');
    $referrer  = sanitize($body['referrer'] ?? '');

    if (!$tid) jsonError('Missing telegram_id.', 400);

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    $isNew  = !$player;

    // ── BAN CHECK: block login for banned users ──────────────────────────────
    if ($player && (int)$player['is_banned'] === 1) {
        jsonError('🚫 Your account has been banned. Please contact support if you believe this is an error.', 403);
    }
    // ─────────────────────────────────────────────────────────────────────────

    if ($isNew) {
        $photoUrl = fetchTelegramPhoto($tid);

        DB::exec(
            'INSERT INTO players (telegram_id, username, first_name, photo_url, photo_updated_at, coins, btc_balance,
             total_withdraw, referrer_id, referrals, referral_coins, faucet_email,
             join_date, level, xp, games_played, best_score, game_points, last_gamepoints_reset)
             VALUES (?,?,?,?,CURDATE(),0,0,0,NULL,0,0,?,NOW(),1,0,0,0,?,CURDATE())',
            [$tid, $username, $firstName, $photoUrl, '', DAILY_GAME_POINTS]
        );

        // Process referral
        if ($referrer && $referrer !== $tid) {
            $referrerPlayer = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$referrer]);
            if ($referrerPlayer) {
                DB::exec(
                    'UPDATE players SET coins = coins + ?, referrals = referrals + 1,
                     referral_coins = referral_coins + ?, game_points = game_points + ?
                     WHERE telegram_id = ?',
                    [REFERRAL_BONUS_COINS, REFERRAL_BONUS_COINS, REFERRAL_GAME_POINTS, $referrer]
                );
                DB::exec('UPDATE players SET referrer_id = ? WHERE telegram_id = ?', [$referrer, $tid]);
                DB::exec(
                    'INSERT IGNORE INTO referrals (referrer_id, referred_id, coins_awarded, game_points_awarded)
                     VALUES (?,?,?,?)',
                    [$referrer, $tid, REFERRAL_BONUS_COINS, REFERRAL_GAME_POINTS]
                );
                notifyReferrer($referrerPlayer, $username);
            }
        }

        $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    } else {
        // Refresh profile photo once per day (Telegram file URLs expire in a few hours)
        $today            = date('Y-m-d');
        $lastPhotoUpdate  = $player['photo_updated_at'] ?? null;
        $needsPhotoRefresh = ($lastPhotoUpdate !== $today);

        if ($needsPhotoRefresh) {
            $freshUrl = fetchTelegramPhoto($tid);
            $photoUrl = $freshUrl ?: $player['photo_url']; // fall back to old URL if fetch fails
            DB::exec(
                'UPDATE players SET username = ?, first_name = ?, photo_url = ?, photo_updated_at = CURDATE() WHERE telegram_id = ?',
                [$username, $firstName, $photoUrl, $tid]
            );
        } else {
            // Photo is still fresh today — just sync name fields
            DB::exec(
                'UPDATE players SET username = ?, first_name = ? WHERE telegram_id = ?',
                [$username, $firstName, $tid]
            );
        }
        $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    }

    $player = refreshGamePoints($player);
    jsonSuccess(['player' => publicPlayer($player), 'is_new' => $isNew]);
}

function handleGetPlayer(?array $tgUser): void {
    $tid = (string)($tgUser['id'] ?? '');
    if (!$tid) jsonError('Unauthorized.', 401);

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    if (!$player) jsonError('Player not found.', 404);

    $player = refreshGamePoints($player);
    jsonSuccess(['player' => publicPlayer($player)]);
}

function handleSubmitScore(array $body, ?array $tgUser): void {
    $tid   = (string)($tgUser['id'] ?? '');
    $score = (int)($body['score'] ?? 0);
    $token = sanitize($body['token'] ?? '');

    if (!$tid) jsonError('Unauthorized.', 401);

    $expectedToken = generateScoreToken($tid, $score, $body['timestamp'] ?? 0);
    if (!hash_equals($expectedToken, $token)) jsonError('Score validation failed.', 403);

    $ts = (int)($body['timestamp'] ?? 0);
    if (abs(time() - $ts) > 120) jsonError('Score timestamp expired.', 403);

    $score = max(0, min($score, MAX_SCORE_PER_GAME));

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    if (!$player) jsonError('Player not found.', 404);

    $player = refreshGamePoints($player);

    if ((int)$player['game_points'] <= 0) {
        jsonError('No game points left. Invite friends to earn more!', 403);
    }

    if ($player['last_game']) {
        $diff = time() - strtotime($player['last_game']);
        if ($diff < GAME_COOLDOWN_SEC) jsonError('Game cooldown active.', 429);
    }

    global $LEVEL_MULTIPLIERS;
    $mult   = $LEVEL_MULTIPLIERS[$player['level']] ?? 1.0;
    $earned = (int)round($score * $mult);

    $newCoins  = $player['coins'] + $earned;
    $newXp     = $player['xp'] + $score;
    $newLevel  = calculateLevel((int)$newXp);
    $newBtc    = (int)floor($newCoins / COINS_PER_SAT);
    $newBest   = max((int)$player['best_score'], $score);
    $newGPts   = max(0, (int)$player['game_points'] - 1);

    DB::exec(
        'UPDATE players SET coins=?, xp=?, level=?, btc_balance=?, best_score=?,
         games_played=games_played+1, last_game=NOW(), game_points=? WHERE telegram_id=?',
        [$newCoins, $newXp, $newLevel, $newBtc, $newBest, $newGPts, $tid]
    );

    DB::exec(
        'INSERT INTO game_sessions (telegram_id, score, coins_earned, level) VALUES (?,?,?,?)',
        [$tid, $score, $earned, $newLevel]
    );

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    jsonSuccess(['coins_earned' => $earned, 'player' => publicPlayer($player)]);
}

function handleDailyBonus(?array $tgUser): void {
    $tid = (string)($tgUser['id'] ?? '');
    if (!$tid) jsonError('Unauthorized.', 401);

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    if (!$player) jsonError('Player not found.', 404);

    if ($player['last_daily']) {
        $lastClaimTs  = strtotime($player['last_daily']);
        $nextClaimTs  = $lastClaimTs + 86400; // exactly 24 hours after last claim
        if (time() < $nextClaimTs) {
            $secsLeft = $nextClaimTs - time();
            jsonError('Daily bonus already claimed. Next in ' . $secsLeft . ' seconds.', 409);
        }
    }

    $newCoins = $player['coins'] + DAILY_BONUS_COINS;
    $newBtc   = (int)floor($newCoins / COINS_PER_SAT);

    DB::exec(
        'UPDATE players SET coins=?, btc_balance=?, last_daily=NOW() WHERE telegram_id=?',
        [$newCoins, $newBtc, $tid]
    );

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    $player = refreshGamePoints($player);
    jsonSuccess(['bonus' => DAILY_BONUS_COINS, 'player' => publicPlayer($player)]);
}


// ─── VERIFY CHANNEL JOIN (getChatMember) ─────────────────────────────────────
/**
 * Called by the frontend after the user taps "Join" on a channel task.
 * Uses Telegram getChatMember to confirm the user is actually a member
 * before granting the reward.
 *
 * Query params: action=verify_channel_join
 * Optional GET param: channel=username  (overrides config default)
 */
function handleVerifyChannelJoin(?array $tgUser): void {
    $tid = (string)($tgUser['id'] ?? '');
    if (!$tid) jsonError('Unauthorized.', 401);

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    if (!$player) jsonError('Player not found.', 404);

    // Allow frontend to pass a specific channel (for multi-channel tasks);
    // fall back to the global config value.
    $channel = sanitize($_GET['channel'] ?? '') ?: CHANNEL_USERNAME;

    if (!$channel) jsonError('Channel not configured.', 500);

    // ── Call Telegram getChatMember ─────────────────────────────────────────
    $url = 'https://api.telegram.org/bot' . BOT_TOKEN . '/getChatMember';
    $res = tgApiCall($url, [
        'chat_id' => '' . ltrim($channel, ''),
        'user_id' => $tid,
    ]);

    if (!$res) {
        // getChatMember failed — bot may not be admin of that channel
        jsonError('Could not verify membership. Make sure the bot is added to the channel as admin.', 502);
    }

    $status   = $res['result']['status'] ?? 'left';
    $isMember = in_array($status, ['member', 'administrator', 'creator'], true);

    if (!$isMember) {
        jsonSuccess(['joined' => false, 'status' => $status]);
        return;
    }

    jsonSuccess(['joined' => true, 'status' => $status]);
}

// ─── TASK REWARD ─────────────────────────────────────────────────────────────
function handleTaskReward(array $body, ?array $tgUser): void {
    $tid    = (string)($tgUser['id'] ?? '');
    $amount = (int)($body['amount'] ?? 0);
    $reason = sanitize($body['reason'] ?? 'Task reward');

    if (!$tid) jsonError('Unauthorized.', 401);
    if ($amount <= 0 || $amount > 20000) jsonError('Invalid reward amount.', 400);

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    if (!$player) jsonError('Player not found.', 404);

    $newCoins = $player['coins'] + $amount;
    $newBtc   = (int)floor($newCoins / COINS_PER_SAT);

    DB::exec(
        'UPDATE players SET coins=?, btc_balance=? WHERE telegram_id=?',
        [$newCoins, $newBtc, $tid]
    );

    DB::exec(
        'INSERT INTO game_sessions (telegram_id, score, coins_earned, level) VALUES (?,?,?,?)',
        [$tid, 0, $amount, (int)$player['level']]
    );

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    $player = refreshGamePoints($player);
    jsonSuccess(['coins_earned' => $amount, 'player' => publicPlayer($player)]);
}

// ─── SAVE FAUCET EMAIL (pre-save on input blur) ───────────────────────────
function handleSaveFaucetEmail(array $body, ?array $tgUser): void {
    $tid   = (string)($tgUser['id'] ?? '');
    $email = sanitize($body['email'] ?? '');

    if (!$tid)  jsonError('Unauthorized.', 401);
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) jsonError('Invalid email.', 400);

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    if (!$player) jsonError('Player not found.', 404);

    DB::exec('UPDATE players SET faucet_email = ? WHERE telegram_id = ?', [$email, $tid]);
    jsonSuccess(['saved' => true, 'email' => $email]);
}

function handleLeaderboard(): void {
    $rows = DB::all(
        'SELECT telegram_id, username, photo_url, coins, level
         FROM players ORDER BY coins DESC LIMIT 20'
    );

    $top = array_values(array_map(function($p, $i) {
        return [
            'rank'      => $i + 1,
            'username'  => $p['username'],
            'photo_url' => $p['photo_url'],
            'coins'     => (int)$p['coins'],
            'level'     => (int)$p['level'],
        ];
    }, $rows, array_keys($rows)));

    jsonSuccess(['leaderboard' => $top]);
}

function handleWithdraw(array $body, ?array $tgUser): void {
    $tid          = (string)($tgUser['id'] ?? '');
    $email        = sanitize($body['email'] ?? '');
    $requestedSat = (int)($body['amount_sat'] ?? 0);

    if (!$tid) jsonError('Unauthorized.', 401);
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) jsonError('Invalid FaucetPay email.', 400);

    $player = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    if (!$player) jsonError('Player not found.', 404);

    // ── BAN CHECK ────────────────────────────────────────────────────────────
    if ((int)$player['is_banned'] === 1) {
        jsonError('🚫 Your account has been banned. Withdrawals are not available.', 403);
    }

    // ── WITHDRAWAL BLOCK CHECK ───────────────────────────────────────────────
    if ((int)$player['withdrawal_blocked'] === 1) {
        jsonError('🔒 Your withdrawal access has been temporarily blocked. Please contact support for assistance.', 403);
    }

    // ── FAUCETPAY EMAIL CONFLICT CHECK ───────────────────────────────────────
    // Block if this email is already registered to a DIFFERENT account
    $emailOwner = DB::row(
        'SELECT telegram_id FROM players WHERE faucet_email = ? AND telegram_id != ?',
        [$email, $tid]
    );
    if ($emailOwner) {
        // Mark this player's withdrawal as blocked due to email conflict
        DB::exec(
            'UPDATE players SET withdrawal_blocked = 1, withdrawal_block_reason = ? WHERE telegram_id = ?',
            ['FaucetPay email already registered to another account.', $tid]
        );
        jsonError('🚫 This FaucetPay email is already linked to another account. Your withdrawal access has been blocked. Contact support if this is a mistake.', 403);
    }
    // ─────────────────────────────────────────────────────────────────────────

    // ── WITHDRAW GATE: first 5 payouts are free; after that require daily tasks ──
    $successRow   = DB::row(
        'SELECT COUNT(*) as cnt FROM withdrawals WHERE telegram_id=? AND status="success"',
        [$tid]
    );
    $successCount = (int)($successRow['cnt'] ?? 0);

    if ($successCount >= FREE_WITHDRAWALS_COUNT) {
        $dailyComplete = (bool)($body['daily_tasks_complete'] ?? false);
        if (!$dailyComplete) {
            jsonError(
                'You have used all ' . FREE_WITHDRAWALS_COUNT . ' free withdrawals. ' .
                'Complete ALL daily tasks today to unlock your next withdrawal.',
                403
            );
        }
    }
    // ─────────────────────────────────────────────────────────────────────────

    $availableSat = (int)floor((int)$player['coins'] / COINS_PER_SAT);

    if ($requestedSat <= 0 || $requestedSat > $availableSat) {
        $sat = $availableSat;
    } else {
        $sat = $requestedSat;
    }

    if ($sat < MIN_WITHDRAW_SAT) {
        jsonError('Minimum withdrawal is ' . MIN_WITHDRAW_SAT . ' SAT. You have ' . $availableSat . ' SAT.', 400);
    }
    if ($sat > MAX_WITHDRAW_SAT) {
        $sat = MAX_WITHDRAW_SAT;
    }

    $coinsUsed    = $sat * COINS_PER_SAT;
    $withdrawalId = 'WD_' . strtoupper(substr(md5(uniqid('', true)), 0, 10));

    DB::exec('UPDATE players SET faucet_email=? WHERE telegram_id=?', [$email, $tid]);

    DB::exec(
        'INSERT INTO withdrawals
         (withdrawal_id, telegram_id, username, photo_url, email, amount_sat, coins_used,
          tx_id, fee_sat, status, created_at)
         VALUES (?,?,?,?,?,?,?,?,?,\'pending\',NOW())',
        [
            $withdrawalId, $tid, $player['username'], $player['photo_url'],
            $email, $sat, $coinsUsed, 'PENDING', 0
        ]
    );

    DB::exec(
        'UPDATE players SET coins=coins-?, btc_balance=FLOOR((coins-?)/?) WHERE telegram_id=?',
        [$coinsUsed, $coinsUsed, COINS_PER_SAT, $tid]
    );

    DB::exec(
        'UPDATE withdrawals SET status=\'processing\' WHERE withdrawal_id=?',
        [$withdrawalId]
    );

    $fpResult = sendFaucetPay($email, $sat);

    if (!$fpResult['success']) {
        DB::exec(
            'UPDATE players SET coins=coins+?, btc_balance=FLOOR((coins+?)/?) WHERE telegram_id=?',
            [$coinsUsed, $coinsUsed, COINS_PER_SAT, $tid]
        );
        DB::exec(
            'UPDATE withdrawals SET status=\'failed\', error_msg=?, processed_at=NOW() WHERE withdrawal_id=?',
            [$fpResult['message'], $withdrawalId]
        );
        jsonError('FaucetPay error: ' . $fpResult['message'], 502);
    }

    $txId = $fpResult['tx_id'] ?? 'N/A';
    DB::exec(
        'UPDATE withdrawals SET status=\'success\', tx_id=?, processed_at=NOW()
         WHERE withdrawal_id=?',
        [$txId, $withdrawalId]
    );

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

    $player     = DB::row('SELECT * FROM players WHERE telegram_id = ?', [$tid]);
    $withdrawal = DB::row('SELECT * FROM withdrawals WHERE withdrawal_id=?', [$withdrawalId]);

    jsonSuccess([
        'withdrawal' => formatWithdrawal($withdrawal),
        'player'     => publicPlayer($player),
    ]);
}

function handleWithdrawStatus(array $body, ?array $tgUser): void {
    $tid          = (string)($tgUser['id'] ?? '');
    $withdrawalId = sanitize($body['withdrawal_id'] ?? '');
    if (!$tid) jsonError('Unauthorized.', 401);

    $w = DB::row(
        'SELECT * FROM withdrawals WHERE withdrawal_id=? AND telegram_id=?',
        [$withdrawalId, $tid]
    );
    if (!$w) jsonError('Withdrawal not found.', 404);

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

function handleWithdrawalHistory(?array $tgUser): void {
    $tid = (string)($tgUser['id'] ?? '');
    if (!$tid) jsonError('Unauthorized.', 401);

    $rows = DB::all(
        'SELECT * FROM withdrawals WHERE telegram_id=? ORDER BY created_at DESC LIMIT 50',
        [$tid]
    );

    // Count successful payouts for gate display
    $successCount = count(array_filter($rows, fn($w) => $w['status'] === 'success'));

    jsonSuccess([
        'withdrawals'    => array_map('formatWithdrawal', $rows),
        'success_count'  => $successCount,
        'free_remaining' => max(0, FREE_WITHDRAWALS_COUNT - $successCount),
    ]);
}

function handleGlobalWithdrawals(): void {
    $since = sanitize($_GET['since'] ?? '');
    $limit = min((int)($_GET['limit'] ?? 20), 50);

    if ($since) {
        $rows = DB::all(
            'SELECT w.*, p.photo_url as p_photo, COALESCE(p.first_name, w.username) AS first_name
             FROM withdrawals w
             LEFT JOIN players p ON p.telegram_id = w.telegram_id
             WHERE w.status=\'success\' AND w.created_at > ?
             ORDER BY w.created_at DESC LIMIT ?',
            [$since, $limit]
        );
    } else {
        $rows = DB::all(
            'SELECT w.*, p.photo_url as p_photo, COALESCE(p.first_name, w.username) AS first_name
             FROM withdrawals w
             LEFT JOIN players p ON p.telegram_id = w.telegram_id
             WHERE w.status=\'success\'
             ORDER BY w.created_at DESC LIMIT ?',
            [$limit]
        );
    }

    $list = array_map(function($w) {
        return [
            'withdrawal_id' => $w['withdrawal_id'],
            'username'      => maskUsername($w['username']),
            'first_name'    => $w['first_name'] ?? $w['username'],
            'photo_url'     => $w['photo_url'] ?? $w['p_photo'] ?? null,
            'amount_sat'    => (int)$w['amount_sat'],
            'created_at'    => $w['created_at'],
            'status'        => $w['status'],
        ];
    }, $rows);

    jsonSuccess(['withdrawals' => $list]);
}

// ═══════════════════════════════════════════════════════════════════════════
// GAME POINTS — Daily refresh helper
// ═══════════════════════════════════════════════════════════════════════════

function refreshGamePoints(array $player): array {
    $today     = date('Y-m-d');
    $lastReset = $player['last_gamepoints_reset'] ?? null;

    if (!$lastReset || $lastReset !== $today) {
        $newPoints = max((int)$player['game_points'], DAILY_GAME_POINTS);
        if ((int)$player['game_points'] < DAILY_GAME_POINTS) {
            $newPoints = DAILY_GAME_POINTS;
        }
        DB::exec(
            'UPDATE players SET game_points=?, last_gamepoints_reset=? WHERE telegram_id=?',
            [$newPoints, $today, $player['telegram_id']]
        );
        $player['game_points']           = $newPoints;
        $player['last_gamepoints_reset'] = $today;
    }
    return $player;
}

// ═══════════════════════════════════════════════════════════════════════════
// FAUCETPAY
// ═══════════════════════════════════════════════════════════════════════════

function sendFaucetPay(string $email, int $satoshi): array {
    $params = http_build_query([
        'api_key'         => FAUCETPAY_API_KEY,
        'to'              => $email,
        'amount'          => $satoshi,
        'currency'        => FAUCETPAY_CURRENCY,
        'referral'        => 'none',
        'activityMessage' => 'BTC Catch Miner Withdrawal',
    ]);

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => FAUCETPAY_API_URL,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $params,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 20,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);

    $res = curl_exec($ch);
    $err = curl_error($ch);
    curl_close($ch);

    if ($err) return ['success' => false, 'message' => 'cURL error: ' . $err];

    $data = json_decode($res, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        return ['success' => false, 'message' => 'Invalid FaucetPay response.'];
    }

    if (($data['status'] ?? 999) === 200) {
        // FaucetPay returns 'payout_id' as the transaction identifier
        $txId = $data['payout_id'] ?? $data['payment_id'] ?? null;
        return ['success' => true, 'tx_id' => $txId ? (string)$txId : 'N/A'];
    }

    return ['success' => false, 'message' => $data['message'] ?? 'FaucetPay error.'];
}

// ═══════════════════════════════════════════════════════════════════════════
// TELEGRAM PHOTO
// ═══════════════════════════════════════════════════════════════════════════

function fetchTelegramPhoto(string $userId): ?string {
    $url  = 'https://api.telegram.org/bot' . BOT_TOKEN . '/getUserProfilePhotos';
    $data = tgApiCall($url, ['user_id' => $userId, 'limit' => 1]);

    if (!$data || empty($data['result']['photos'])) return null;

    $fileId = $data['result']['photos'][0][0]['file_id'] ?? null;
    if (!$fileId) return null;

    $url2  = 'https://api.telegram.org/bot' . BOT_TOKEN . '/getFile';
    $data2 = tgApiCall($url2, ['file_id' => $fileId]);

    $filePath = $data2['result']['file_path'] ?? null;
    if (!$filePath) return null;

    return 'https://api.telegram.org/file/bot' . BOT_TOKEN . '/' . $filePath;
}

function tgApiCall(string $url, array $params): ?array {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $url . '?' . http_build_query($params),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 8,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    $res = curl_exec($ch);
    curl_close($ch);
    if (!$res) return null;
    $data = json_decode($res, true);
    return ($data['ok'] ?? false) ? $data : null;
}

// ═══════════════════════════════════════════════════════════════════════════
// BOT NOTIFICATION
// ═══════════════════════════════════════════════════════════════════════════

function notifyReferrer(array $referrer, string $newUsername): void {
    $msg  = "🎉 *New referral joined!*\n\n";
    $msg .= "👤 User: @{$newUsername}\n";
    $msg .= "💰 You earned *+" . REFERRAL_BONUS_COINS . " coins!*\n";
    $msg .= "🎮 You earned *+" . REFERRAL_GAME_POINTS . " game points!*\n\n";
    $msg .= "Keep sharing to earn more! 🚀";

    $url = 'https://api.telegram.org/bot' . BOT_TOKEN . '/sendMessage';
    $ch  = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode([
            'chat_id'    => $referrer['telegram_id'],
            'text'       => $msg,
            'parse_mode' => 'Markdown',
        ]),
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 8,
    ]);
    curl_exec($ch);
    curl_close($ch);
}

// ═══════════════════════════════════════════════════════════════════════════
// TELEGRAM AUTH
// ═══════════════════════════════════════════════════════════════════════════

function validateTelegramInitData(string $initData): ?array {
    parse_str($initData, $params);
    $hash = $params['hash'] ?? '';
    unset($params['hash']);

    ksort($params);
    $dataCheckString = '';
    foreach ($params as $k => $v) {
        $dataCheckString .= "{$k}={$v}\n";
    }
    $dataCheckString = rtrim($dataCheckString, "\n");

    $secretKey    = hash_hmac('sha256', BOT_TOKEN, 'WebAppData', true);
    $computedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

    if (!hash_equals($computedHash, $hash)) return null;

    $authDate = (int)($params['auth_date'] ?? 0);
    if ((time() - $authDate) > 86400) return null;

    $user = json_decode($params['user'] ?? '{}', true);
    return $user ?: null;
}

// ═══════════════════════════════════════════════════════════════════════════
// RATE LIMITING
// ═══════════════════════════════════════════════════════════════════════════

function checkRateLimit(string $ip): bool {
    $now = time();
    try {
        $row = DB::row('SELECT count, window_start FROM rate_limits WHERE ip=?', [$ip]);
        if (!$row || ($now - (int)$row['window_start']) > RATE_LIMIT_WINDOW) {
            DB::exec(
                'INSERT INTO rate_limits (ip, count, window_start) VALUES (?,1,?)
                 ON DUPLICATE KEY UPDATE count=1, window_start=?',
                [$ip, $now, $now]
            );
            return true;
        }
        $count = (int)$row['count'] + 1;
        DB::exec('UPDATE rate_limits SET count=? WHERE ip=?', [$count, $ip]);
        return $count <= RATE_LIMIT_MAX;
    } catch (Exception $e) {
        return true;
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// LEVEL SYSTEM
// ═══════════════════════════════════════════════════════════════════════════

function calculateLevel(int $xp): int {
    global $LEVEL_THRESHOLDS;
    $lvl = 1;
    foreach ($LEVEL_THRESHOLDS as $level => $threshold) {
        if ($xp >= $threshold) $lvl = $level;
    }
    return $lvl;
}

// ═══════════════════════════════════════════════════════════════════════════
// DATA FORMATTERS
// ═══════════════════════════════════════════════════════════════════════════

function publicPlayer(array $p): array {
    $canDaily = canClaimDaily($p['last_daily'] ?? null);

    // Seconds until next daily claim (for countdown) — exactly 24h from last claim
    $nextDailySeconds = 0;
    if (!$canDaily && $p['last_daily']) {
        $nextClaimTs      = strtotime($p['last_daily']) + 86400;
        $nextDailySeconds = max(0, $nextClaimTs - time());
    }

    return [
        'telegram_id'        => $p['telegram_id'],
        'username'           => $p['username'],
        'first_name'         => $p['first_name'] ?? '',
        'photo_url'          => $p['photo_url'] ?? null,
        'coins'              => (int)$p['coins'],
        'btc_balance'        => (int)$p['btc_balance'],
        'total_withdraw'     => (int)$p['total_withdraw'],
        'referrals'          => (int)$p['referrals'],
        'referral_coins'     => (int)$p['referral_coins'],
        'faucet_email'       => $p['faucet_email'] ?? '',
        'level'              => (int)$p['level'],
        'xp'                 => (int)$p['xp'],
        'games_played'       => (int)$p['games_played'],
        'best_score'         => (int)$p['best_score'],
        'join_date'          => $p['join_date'],
        'last_daily'         => $p['last_daily'],
        'can_daily'          => $canDaily,
        'next_daily_seconds' => $nextDailySeconds,  // countdown for UI
        'game_points'        => (int)($p['game_points'] ?? 0),
    ];
}

function formatWithdrawal(array $w): array {
    return [
        'id'           => $w['withdrawal_id'],
        'telegram_id'  => $w['telegram_id'],
        'username'     => $w['username'],
        'photo_url'    => $w['photo_url'] ?? null,
        'email'        => $w['email'],
        'amount_sat'   => (int)$w['amount_sat'],
        'coins_used'   => (int)$w['coins_used'],
        'tx_id'        => $w['tx_id'],
        'fee_sat'      => (int)$w['fee_sat'],
        'status'       => $w['status'],
        'error_msg'    => $w['error_msg'],
        'date'         => $w['created_at'],
        'processed_at' => $w['processed_at'],
    ];
}

function canClaimDaily(?string $lastDaily): bool {
    if (!$lastDaily) return true;
    // Allow claim only if 24 hours have passed since last claim
    return (time() - strtotime($lastDaily)) >= 86400;
}

function maskUsername(string $name): string {
    if (strlen($name) <= 3) return $name . '***';
    return substr($name, 0, 3) . str_repeat('*', min(strlen($name) - 3, 4));
}

// ═══════════════════════════════════════════════════════════════════════════
// SECURITY HELPERS
// ═══════════════════════════════════════════════════════════════════════════

function generateScoreToken(string $tid, int $score, int $ts): string {
    return hash_hmac('sha256', "{$tid}:{$score}:{$ts}", GAME_SECRET);
}

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

// ═══════════════════════════════════════════════════════════════════════════
// RESPONSE
// ═══════════════════════════════════════════════════════════════════════════

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

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