<?php
declare(strict_types=1);

require_once __DIR__ . '/db.php';

/** Her satır: o seviyeye çıkmak için gereken XP (üst üste toplam değil). */
function mocker_level_default_levels(): array
{
    return [
        ['level' => 1, 'xp_required' => 1000, 'coin_reward' => 50],
        ['level' => 2, 'xp_required' => 1500, 'coin_reward' => 100],
        ['level' => 3, 'xp_required' => 2000, 'coin_reward' => 175],
        ['level' => 4, 'xp_required' => 2500, 'coin_reward' => 250],
        ['level' => 5, 'xp_required' => 3000, 'coin_reward' => 350],
        ['level' => 6, 'xp_required' => 4000, 'coin_reward' => 500],
        ['level' => 7, 'xp_required' => 5000, 'coin_reward' => 700],
        ['level' => 8, 'xp_required' => 6500, 'coin_reward' => 900],
        ['level' => 9, 'xp_required' => 8000, 'coin_reward' => 1200],
        ['level' => 10, 'xp_required' => 10000, 'coin_reward' => 1600],
    ];
}

function mocker_level_default_settings(): array
{
    return [
        'enabled' => true,
        'xp_game_round' => 2,
        'xp_ticket_join' => 8,
        'xp_ticket_request' => 6,
        'xp_site_active_daily' => 4,
        'xp_telegram_message' => 1,
        'telegram_daily_cap' => 30,
    ];
}

function mocker_level_fast_defaults_signature(): array
{
    return [
        'xp_game_round' => 8,
        'xp_ticket_join' => 25,
        'xp_ticket_request' => 18,
        'xp_site_active_daily' => 15,
        'xp_telegram_message' => 5,
        'telegram_daily_cap' => 150,
        'levels' => [
            ['level' => 1, 'xp_required' => 500, 'coin_reward' => 50],
            ['level' => 2, 'xp_required' => 1000, 'coin_reward' => 100],
            ['level' => 3, 'xp_required' => 1500, 'coin_reward' => 175],
            ['level' => 4, 'xp_required' => 2000, 'coin_reward' => 250],
            ['level' => 5, 'xp_required' => 3000, 'coin_reward' => 350],
            ['level' => 6, 'xp_required' => 4000, 'coin_reward' => 500],
            ['level' => 7, 'xp_required' => 5000, 'coin_reward' => 700],
            ['level' => 8, 'xp_required' => 6000, 'coin_reward' => 900],
            ['level' => 9, 'xp_required' => 7000, 'coin_reward' => 1200],
            ['level' => 10, 'xp_required' => 8000, 'coin_reward' => 1600],
        ],
    ];
}

function mocker_level_migrate(PDO $pdo): void
{
    $pdo->exec(
        'CREATE TABLE IF NOT EXISTS level_settings (
            id INTEGER PRIMARY KEY CHECK (id = 1),
            enabled INTEGER NOT NULL DEFAULT 1,
            xp_game_round INTEGER NOT NULL DEFAULT 8,
            xp_ticket_join INTEGER NOT NULL DEFAULT 25,
            xp_ticket_request INTEGER NOT NULL DEFAULT 18,
            xp_site_active_daily INTEGER NOT NULL DEFAULT 15,
            xp_telegram_message INTEGER NOT NULL DEFAULT 5,
            telegram_daily_cap INTEGER NOT NULL DEFAULT 150,
            levels_json TEXT NOT NULL DEFAULT \'[]\',
            levels_per_level INTEGER NOT NULL DEFAULT 1,
            updated_at INTEGER NOT NULL DEFAULT 0
        )'
    );
    try {
        $pdo->exec('ALTER TABLE level_settings ADD COLUMN levels_per_level INTEGER NOT NULL DEFAULT 0');
    } catch (Throwable $e) {
        /* column exists */
    }
    $pdo->exec(
        'CREATE TABLE IF NOT EXISTS user_level_state (
            user_id INTEGER PRIMARY KEY,
            xp_total INTEGER NOT NULL DEFAULT 0,
            level_no INTEGER NOT NULL DEFAULT 0,
            awarded_level_no INTEGER NOT NULL DEFAULT 0,
            updated_at INTEGER NOT NULL DEFAULT 0,
            FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
        )'
    );
    $pdo->exec(
        'CREATE TABLE IF NOT EXISTS user_level_xp_events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL,
            source TEXT NOT NULL,
            source_key TEXT,
            xp INTEGER NOT NULL,
            created_at INTEGER NOT NULL,
            meta_json TEXT,
            FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
        )'
    );
    $pdo->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_user_level_xp_source_key ON user_level_xp_events(source_key)');
    $pdo->exec('CREATE INDEX IF NOT EXISTS idx_user_level_xp_user ON user_level_xp_events(user_id, created_at DESC)');

    $st = $pdo->query('SELECT COUNT(*) FROM level_settings');
    $cnt = (int) $st->fetchColumn();
    if ($cnt <= 0) {
        $d = mocker_level_default_settings();
        $levelsJson = json_encode(mocker_level_default_levels(), JSON_UNESCAPED_UNICODE);
        if ($levelsJson === false) {
            $levelsJson = '[]';
        }
        $ins = $pdo->prepare(
            'INSERT INTO level_settings (
                id, enabled, xp_game_round, xp_ticket_join, xp_ticket_request, xp_site_active_daily,
                xp_telegram_message, telegram_daily_cap, levels_json, levels_per_level, updated_at
            ) VALUES (1, :en, :xg, :xj, :xr, :xs, :xt, :cap, :lv, 1, :t)'
        );
        $ins->execute([
            ':en' => $d['enabled'] ? 1 : 0,
            ':xg' => (int) $d['xp_game_round'],
            ':xj' => (int) $d['xp_ticket_join'],
            ':xr' => (int) $d['xp_ticket_request'],
            ':xs' => (int) $d['xp_site_active_daily'],
            ':xt' => (int) $d['xp_telegram_message'],
            ':cap' => (int) $d['telegram_daily_cap'],
            ':lv' => $levelsJson,
            ':t' => time(),
        ]);
    }

    mocker_level_migrate_levels_schema($pdo);
}

/** Eski kümülatif XP eşiklerini seviye başına XP'ye çevirir (bir kez). */
function mocker_level_migrate_levels_schema(PDO $pdo): void
{
    $row = $pdo->query('SELECT levels_json, levels_per_level FROM level_settings WHERE id = 1')->fetch(PDO::FETCH_ASSOC);
    if (!$row || (int) ($row['levels_per_level'] ?? 1) === 1) {
        return;
    }
    $decoded = json_decode((string) ($row['levels_json'] ?? '[]'), true);
    if (!is_array($decoded) || $decoded === []) {
        $pdo->exec('UPDATE level_settings SET levels_per_level = 1 WHERE id = 1');
        return;
    }
    $levels = mocker_level_convert_cumulative_levels(mocker_level_normalize_levels($decoded));
    $levelsJson = json_encode($levels, JSON_UNESCAPED_UNICODE);
    if ($levelsJson === false) {
        $levelsJson = '[]';
    }
    $upd = $pdo->prepare('UPDATE level_settings SET levels_json = :lv, levels_per_level = 1, updated_at = :t WHERE id = 1');
    $upd->execute([':lv' => $levelsJson, ':t' => time()]);

    mocker_level_recalc_all_user_levels($pdo, $levels);
}

/**
 * Sadece eski "hızlı" varsayılanları kullanan kurulumlarda dengeyi düşürür.
 * Admin özel ayar yaptıysa dokunmaz.
 */
function mocker_level_rebalance_if_using_fast_defaults(PDO $pdo): void
{
    $row = $pdo->query('SELECT * FROM level_settings WHERE id = 1')->fetch(PDO::FETCH_ASSOC);
    if (!$row) {
        return;
    }
    $sig = mocker_level_fast_defaults_signature();
    $levelsRaw = json_decode((string) ($row['levels_json'] ?? '[]'), true);
    if (!is_array($levelsRaw)) {
        return;
    }
    $levelsCurrent = mocker_level_normalize_levels($levelsRaw);
    $levelsFast = mocker_level_normalize_levels($sig['levels']);

    $isFast =
        (int) ($row['xp_game_round'] ?? -1) === $sig['xp_game_round'] &&
        (int) ($row['xp_ticket_join'] ?? -1) === $sig['xp_ticket_join'] &&
        (int) ($row['xp_ticket_request'] ?? -1) === $sig['xp_ticket_request'] &&
        (int) ($row['xp_site_active_daily'] ?? -1) === $sig['xp_site_active_daily'] &&
        (int) ($row['xp_telegram_message'] ?? -1) === $sig['xp_telegram_message'] &&
        (int) ($row['telegram_daily_cap'] ?? -1) === $sig['telegram_daily_cap'] &&
        $levelsCurrent === $levelsFast;

    if (!$isFast) {
        return;
    }

    $slow = mocker_level_default_settings();
    $slowLevels = mocker_level_default_levels();
    $levelsJson = json_encode($slowLevels, JSON_UNESCAPED_UNICODE);
    if ($levelsJson === false) {
        return;
    }
    $upd = $pdo->prepare(
        'UPDATE level_settings SET
            xp_game_round = :xg,
            xp_ticket_join = :xj,
            xp_ticket_request = :xr,
            xp_site_active_daily = :xs,
            xp_telegram_message = :xt,
            telegram_daily_cap = :cap,
            levels_json = :lv,
            updated_at = :t
         WHERE id = 1'
    );
    $upd->execute([
        ':xg' => (int) $slow['xp_game_round'],
        ':xj' => (int) $slow['xp_ticket_join'],
        ':xr' => (int) $slow['xp_ticket_request'],
        ':xs' => (int) $slow['xp_site_active_daily'],
        ':xt' => (int) $slow['xp_telegram_message'],
        ':cap' => (int) $slow['telegram_daily_cap'],
        ':lv' => $levelsJson,
        ':t' => time(),
    ]);

    mocker_level_recalc_all_user_levels($pdo, $slowLevels);
}

function mocker_level_recalc_all_user_levels(PDO $pdo, array $levels): void
{
    $rows = $pdo->query('SELECT user_id, xp_total, awarded_level_no FROM user_level_state')->fetchAll(PDO::FETCH_ASSOC);
    if (!$rows) {
        return;
    }
    $upd = $pdo->prepare(
        'UPDATE user_level_state SET level_no = :lv, updated_at = :t WHERE user_id = :u'
    );
    $now = time();
    foreach ($rows as $row) {
        $uid = (int) ($row['user_id'] ?? 0);
        if ($uid <= 0) {
            continue;
        }
        $xp = max(0, (int) ($row['xp_total'] ?? 0));
        $lv = mocker_level_calc_level_from_xp($xp, $levels);
        $upd->execute([':lv' => $lv, ':t' => $now, ':u' => $uid]);
    }
}

function mocker_level_convert_cumulative_levels(array $levels): array
{
    usort($levels, static function (array $a, array $b): int {
        return ((int) ($a['level'] ?? 0)) <=> ((int) ($b['level'] ?? 0));
    });
    $prev = 0;
    foreach ($levels as &$it) {
        $req = max(0, (int) ($it['xp_required'] ?? 0));
        $it['xp_required'] = max(0, $req - $prev);
        $prev = $req;
    }
    unset($it);

    return $levels;
}

function mocker_level_get_settings(PDO $pdo): array
{
    mocker_level_migrate($pdo);
    mocker_level_migrate_levels_schema($pdo);
    mocker_level_rebalance_if_using_fast_defaults($pdo);
    $row = $pdo->query('SELECT * FROM level_settings WHERE id = 1')->fetch(PDO::FETCH_ASSOC) ?: [];
    $defaults = mocker_level_default_settings();
    $levels = mocker_level_default_levels();
    $decoded = json_decode((string) ($row['levels_json'] ?? '[]'), true);
    if (is_array($decoded) && !empty($decoded)) {
        $levels = mocker_level_normalize_levels($decoded);
    }

    return [
        'enabled' => (int) ($row['enabled'] ?? ($defaults['enabled'] ? 1 : 0)) === 1,
        'xp_game_round' => max(0, (int) ($row['xp_game_round'] ?? $defaults['xp_game_round'])),
        'xp_ticket_join' => max(0, (int) ($row['xp_ticket_join'] ?? $defaults['xp_ticket_join'])),
        'xp_ticket_request' => max(0, (int) ($row['xp_ticket_request'] ?? $defaults['xp_ticket_request'])),
        'xp_site_active_daily' => max(0, (int) ($row['xp_site_active_daily'] ?? $defaults['xp_site_active_daily'])),
        'xp_telegram_message' => max(0, (int) ($row['xp_telegram_message'] ?? $defaults['xp_telegram_message'])),
        'telegram_daily_cap' => max(1, (int) ($row['telegram_daily_cap'] ?? $defaults['telegram_daily_cap'])),
        'levels' => $levels,
    ];
}

function mocker_level_normalize_levels(array $levels): array
{
    $out = [];
    foreach ($levels as $it) {
        if (!is_array($it)) {
            continue;
        }
        $lv = max(1, (int) ($it['level'] ?? 0));
        $xp = max(0, (int) ($it['xp_required'] ?? 0));
        $coin = max(0, (int) ($it['coin_reward'] ?? 0));
        $out[] = ['level' => $lv, 'xp_required' => $xp, 'coin_reward' => $coin];
    }
    if (!$out) {
        return mocker_level_default_levels();
    }
    usort($out, static function (array $a, array $b): int {
        if ($a['xp_required'] === $b['xp_required']) {
            return $a['level'] <=> $b['level'];
        }
        return $a['xp_required'] <=> $b['xp_required'];
    });
    $dedup = [];
    foreach ($out as $it) {
        $dedup[(string) $it['level']] = $it;
    }
    return array_values($dedup);
}

function mocker_level_upsert_settings(PDO $pdo, array $payload): array
{
    mocker_level_migrate($pdo);
    $curr = mocker_level_get_settings($pdo);
    $next = [
        'enabled' => isset($payload['enabled']) ? (bool) $payload['enabled'] : $curr['enabled'],
        'xp_game_round' => isset($payload['xp_game_round']) ? max(0, (int) $payload['xp_game_round']) : $curr['xp_game_round'],
        'xp_ticket_join' => isset($payload['xp_ticket_join']) ? max(0, (int) $payload['xp_ticket_join']) : $curr['xp_ticket_join'],
        'xp_ticket_request' => isset($payload['xp_ticket_request']) ? max(0, (int) $payload['xp_ticket_request']) : $curr['xp_ticket_request'],
        'xp_site_active_daily' => isset($payload['xp_site_active_daily']) ? max(0, (int) $payload['xp_site_active_daily']) : $curr['xp_site_active_daily'],
        'xp_telegram_message' => isset($payload['xp_telegram_message']) ? max(0, (int) $payload['xp_telegram_message']) : $curr['xp_telegram_message'],
        'telegram_daily_cap' => isset($payload['telegram_daily_cap']) ? max(1, (int) $payload['telegram_daily_cap']) : $curr['telegram_daily_cap'],
        'levels' => isset($payload['levels']) && is_array($payload['levels'])
            ? mocker_level_normalize_levels($payload['levels'])
            : $curr['levels'],
    ];
    $levelsJson = json_encode($next['levels'], JSON_UNESCAPED_UNICODE);
    if ($levelsJson === false) {
        $levelsJson = json_encode(mocker_level_default_levels(), JSON_UNESCAPED_UNICODE);
    }
    $upd = $pdo->prepare(
        'UPDATE level_settings SET
            enabled = :en,
            xp_game_round = :xg,
            xp_ticket_join = :xj,
            xp_ticket_request = :xr,
            xp_site_active_daily = :xs,
            xp_telegram_message = :xt,
            telegram_daily_cap = :cap,
            levels_json = :lv,
            updated_at = :t
         WHERE id = 1'
    );
    $upd->execute([
        ':en' => $next['enabled'] ? 1 : 0,
        ':xg' => $next['xp_game_round'],
        ':xj' => $next['xp_ticket_join'],
        ':xr' => $next['xp_ticket_request'],
        ':xs' => $next['xp_site_active_daily'],
        ':xt' => $next['xp_telegram_message'],
        ':cap' => $next['telegram_daily_cap'],
        ':lv' => $levelsJson,
        ':t' => time(),
    ]);
    return $next;
}

/** Tamamlanan seviyelerin XP maliyetleri toplamı (seviye 0 → $levelNo). */
function mocker_level_xp_spent_to_reach(int $levelNo, array $levels): int
{
    $sum = 0;
    foreach ($levels as $it) {
        $lv = (int) ($it['level'] ?? 0);
        if ($lv >= 1 && $lv <= $levelNo) {
            $sum += max(0, (int) ($it['xp_required'] ?? 0));
        }
    }

    return $sum;
}

/** Toplam XP'den mevcut seviye (her seviye = o satırdaki xp_required kadar ilerleme). */
function mocker_level_calc_level_from_xp(int $xpTotal, array $levels): int
{
    $sorted = mocker_level_normalize_levels($levels);
    $lv = 0;
    $spent = 0;
    foreach ($sorted as $it) {
        $need = max(0, (int) ($it['xp_required'] ?? 0));
        $targetLv = (int) ($it['level'] ?? 0);
        if ($targetLv <= 0) {
            continue;
        }
        if ($need <= 0) {
            if ($xpTotal >= $spent) {
                $lv = max($lv, $targetLv);
            }
            continue;
        }
        if ($xpTotal - $spent < $need) {
            break;
        }
        $spent += $need;
        $lv = $targetLv;
    }

    return $lv;
}

function mocker_level_next_level_def(int $levelNo, array $levels): ?array
{
    $next = $levelNo + 1;
    foreach ($levels as $it) {
        if ((int) ($it['level'] ?? 0) === $next) {
            return $it;
        }
    }

    return null;
}

function mocker_level_reward_for_range(array $levels, int $fromExclusive, int $toInclusive): int
{
    if ($toInclusive <= $fromExclusive) {
        return 0;
    }
    $sum = 0;
    foreach ($levels as $it) {
        $lv = (int) ($it['level'] ?? 0);
        if ($lv <= $fromExclusive || $lv > $toInclusive) {
            continue;
        }
        $sum += max(0, (int) ($it['coin_reward'] ?? 0));
    }
    return $sum;
}

function mocker_level_get_user_state(PDO $pdo, int $userId): array
{
    mocker_level_migrate($pdo);
    $st = $pdo->prepare('SELECT xp_total, level_no, awarded_level_no FROM user_level_state WHERE user_id = :u LIMIT 1');
    $st->execute([':u' => $userId]);
    $row = $st->fetch(PDO::FETCH_ASSOC);
    if ($row) {
        return [
            'xp_total' => max(0, (int) ($row['xp_total'] ?? 0)),
            'level_no' => max(0, (int) ($row['level_no'] ?? 0)),
            'awarded_level_no' => max(0, (int) ($row['awarded_level_no'] ?? 0)),
        ];
    }
    return ['xp_total' => 0, 'level_no' => 0, 'awarded_level_no' => 0];
}

function mocker_level_store_user_state(PDO $pdo, int $userId, array $state): void
{
    $up = $pdo->prepare(
        'INSERT INTO user_level_state (user_id, xp_total, level_no, awarded_level_no, updated_at)
         VALUES (:u, :xp, :lv, :aw, :t)
         ON CONFLICT(user_id) DO UPDATE SET
            xp_total = excluded.xp_total,
            level_no = excluded.level_no,
            awarded_level_no = excluded.awarded_level_no,
            updated_at = excluded.updated_at'
    );
    $up->execute([
        ':u' => $userId,
        ':xp' => max(0, (int) ($state['xp_total'] ?? 0)),
        ':lv' => max(0, (int) ($state['level_no'] ?? 0)),
        ':aw' => max(0, (int) ($state['awarded_level_no'] ?? 0)),
        ':t' => time(),
    ]);
}

function mocker_level_grant_xp(PDO $pdo, int $userId, string $source, int $xp, ?string $sourceKey = null, array $meta = []): array
{
    if ($userId <= 0 || $xp <= 0) {
        return ['ok' => false, 'changed' => false, 'reason' => 'bad_input'];
    }
    $settings = mocker_level_get_settings($pdo);
    if (!$settings['enabled']) {
        return ['ok' => true, 'changed' => false, 'reason' => 'disabled'];
    }
    $metaJson = json_encode($meta, JSON_UNESCAPED_UNICODE);
    if ($metaJson === false) {
        $metaJson = '{}';
    }
    $ins = $pdo->prepare(
        'INSERT INTO user_level_xp_events (user_id, source, source_key, xp, created_at, meta_json)
         VALUES (:u, :s, :k, :x, :t, :m)'
    );
    try {
        $ins->execute([
            ':u' => $userId,
            ':s' => substr($source, 0, 64),
            ':k' => $sourceKey !== null && $sourceKey !== '' ? substr($sourceKey, 0, 190) : null,
            ':x' => $xp,
            ':t' => time(),
            ':m' => $metaJson,
        ]);
    } catch (Throwable $e) {
        if ($sourceKey !== null && $sourceKey !== '' && str_contains(strtolower($e->getMessage()), 'unique')) {
            return ['ok' => true, 'changed' => false, 'reason' => 'duplicate'];
        }
        throw $e;
    }

    $state = mocker_level_get_user_state($pdo, $userId);
    $beforeLevel = (int) $state['level_no'];
    $beforeAwarded = (int) $state['awarded_level_no'];
    $state['xp_total'] = (int) $state['xp_total'] + $xp;
    $state['level_no'] = mocker_level_calc_level_from_xp((int) $state['xp_total'], $settings['levels']);
    $coinsAwarded = 0;
    if ($state['level_no'] > $beforeAwarded) {
        $coinsAwarded = mocker_level_reward_for_range($settings['levels'], $beforeAwarded, (int) $state['level_no']);
        $state['awarded_level_no'] = (int) $state['level_no'];
        if ($coinsAwarded > 0) {
            $updCoins = $pdo->prepare('UPDATE users SET coins = coins + :c WHERE id = :u');
            $updCoins->execute([':c' => $coinsAwarded, ':u' => $userId]);
        }
    }
    mocker_level_store_user_state($pdo, $userId, $state);
    return [
        'ok' => true,
        'changed' => true,
        'xp_total' => (int) $state['xp_total'],
        'level_no' => (int) $state['level_no'],
        'leveled_up' => (int) $state['level_no'] > $beforeLevel,
        'coins_awarded' => $coinsAwarded,
    ];
}

function mocker_level_progress_for_user(PDO $pdo, int $userId): array
{
    $settings = mocker_level_get_settings($pdo);
    $state = mocker_level_get_user_state($pdo, $userId);
    $levels = $settings['levels'];
    $currXp = (int) $state['xp_total'];
    $currLv = mocker_level_calc_level_from_xp($currXp, $levels);
    $spent = mocker_level_xp_spent_to_reach($currLv, $levels);
    $xpInLevel = max(0, $currXp - $spent);
    $nextDef = mocker_level_next_level_def($currLv, $levels);
    $need = $nextDef !== null ? max(0, (int) ($nextDef['xp_required'] ?? 0)) : 0;
    $xpToNext = $need > 0 ? max(0, $need - $xpInLevel) : 0;
    $progress = $need > 0 ? (int) floor(min(100, max(0, ($xpInLevel / $need) * 100))) : 100;

    return [
        'enabled' => $settings['enabled'],
        'level' => $currLv,
        'xpTotal' => $currXp,
        'xpInLevel' => $xpInLevel,
        'xpToNext' => $xpToNext,
        'xpNeeded' => $need,
        'progressPercent' => $progress,
        'nextLevel' => $nextDef === null ? null : (int) ($nextDef['level'] ?? ($currLv + 1)),
    ];
}

function mocker_level_award_site_daily(PDO $pdo, int $userId): void
{
    $settings = mocker_level_get_settings($pdo);
    $xp = max(0, (int) $settings['xp_site_active_daily']);
    if ($xp <= 0) {
        return;
    }
    $day = gmdate('Ymd');
    mocker_level_grant_xp($pdo, $userId, 'site_daily', $xp, "site_daily:{$userId}:{$day}");
}

function mocker_level_award_game_round(PDO $pdo, int $userId, string $game, int $roundId): void
{
    $settings = mocker_level_get_settings($pdo);
    $xp = max(0, (int) $settings['xp_game_round']);
    if ($xp <= 0 || $roundId <= 0) {
        return;
    }
    $source = in_array($game, ['blackjack', 'slot', 'roulette'], true) ? "game_{$game}" : 'game_round';
    mocker_level_grant_xp($pdo, $userId, $source, $xp, "{$source}:{$roundId}");
}

function mocker_level_award_ticket_join(PDO $pdo, int $userId, int $entryId): void
{
    $settings = mocker_level_get_settings($pdo);
    $xp = max(0, (int) $settings['xp_ticket_join']);
    if ($xp <= 0 || $entryId <= 0) {
        return;
    }
    mocker_level_grant_xp($pdo, $userId, 'ticket_join', $xp, "ticket_join:{$entryId}");
}

function mocker_level_award_ticket_request(PDO $pdo, int $userId, int $requestId): void
{
    $settings = mocker_level_get_settings($pdo);
    $xp = max(0, (int) $settings['xp_ticket_request']);
    if ($xp <= 0 || $requestId <= 0) {
        return;
    }
    mocker_level_grant_xp($pdo, $userId, 'ticket_request', $xp, "ticket_request:{$requestId}");
}

function mocker_level_award_telegram_message(PDO $pdo, int $telegramUserId, int $updateId): void
{
    if ($telegramUserId <= 0 || $updateId <= 0) {
        return;
    }
    $settings = mocker_level_get_settings($pdo);
    $xpPer = max(0, (int) $settings['xp_telegram_message']);
    if ($xpPer <= 0) {
        return;
    }
    $st = $pdo->prepare('SELECT id FROM users WHERE telegram_user_id = :tid LIMIT 1');
    $st->execute([':tid' => $telegramUserId]);
    $uid = (int) $st->fetchColumn();
    if ($uid <= 0) {
        return;
    }
    $day = gmdate('Ymd');
    $cap = max(1, (int) $settings['telegram_daily_cap']);
    $q = $pdo->prepare(
        'SELECT COALESCE(SUM(xp),0) FROM user_level_xp_events
         WHERE user_id = :u AND source = :s AND source_key LIKE :k'
    );
    $q->execute([':u' => $uid, ':s' => 'telegram_message', ':k' => "telegram_message:{$uid}:{$day}:%"]);
    $todayXp = (int) $q->fetchColumn();
    if ($todayXp >= $cap) {
        return;
    }
    $grant = min($xpPer, $cap - $todayXp);
    if ($grant <= 0) {
        return;
    }
    mocker_level_grant_xp(
        $pdo,
        $uid,
        'telegram_message',
        $grant,
        "telegram_message:{$uid}:{$day}:{$updateId}"
    );
}
