<?php
/**
 * Unified Telegram bot cron runner.
 *
 * Put ONLY this file in cPanel Cron Jobs and run it every minute.
 * Example:
 * /usr/bin/curl -s "https://YOUR-DOMAIN/YOUR-BOT-FOLDER/cron_all_telegram.php?task=auto&broadcast_limit=20&broadcast_rps=20" >/dev/null 2>&1
 * Nightly backup/report only:
 * /usr/bin/curl -fsS "https://YOUR-DOMAIN/YOUR-BOT-FOLDER/cron_all_telegram.php?task=nightly&force=1&left_notify=1" >/dev/null 2>&1
 *
 * Supported tasks:
 * task=auto                 broadcast + cache + left with safe intervals
 * task=broadcast            only queued broadcast/forward sending
 * task=cache                only cache warmup
 * task=left                 only left-channel penalty check
 * task=backup               only database backup
 * task=broadcast,cache,left selected tasks
 * task=nightly              DB backup + stepwise left penalty check + admin report + accounting daily report
 * task=accounting_daily     only daily accounting report to accounting channel
 */
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');

if (PHP_SAPI !== 'cli') {
    header('Content-Type: application/json; charset=utf-8');
}

$root = __DIR__;
@chdir($root);

// Allow CLI usage like: php cron_all_telegram.php task=auto broadcast_limit=20
if (PHP_SAPI === 'cli' && !empty($argv) && is_array($argv)) {
    foreach (array_slice($argv, 1) as $arg) {
        if (strpos($arg, '=') !== false) {
            [$k, $v] = explode('=', $arg, 2);
            $_GET[$k] = $v;
        }
    }
}

if (is_file($root . '/config.php')) {
    require_once $root . '/config.php';
}

if (defined('CRON_SECRET') && CRON_SECRET !== '') {
    $key = isset($_GET['key']) ? (string)$_GET['key'] : '';
    if (!hash_equals((string)CRON_SECRET, $key)) {
        http_response_code(403);
        echo json_encode(['ok' => false, 'error' => 'invalid_cron_key'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        exit;
    }
}

$cacheDir = $root . '/cache';
if (!is_dir($cacheDir)) @mkdir($cacheDir, 0775, true);
ini_set('error_log', $root . '/debug.log');

$lockPath = $cacheDir . '/cron_all.lock';
$lock = @fopen($lockPath, 'c+');
if ($lock && !@flock($lock, LOCK_EX | LOCK_NB)) {
    echo json_encode(['ok' => true, 'skipped' => true, 'reason' => 'previous_master_cron_running'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    exit;
}
register_shutdown_function(function () use ($lock) {
    if ($lock) {
        @flock($lock, LOCK_UN);
        @fclose($lock);
    }
});

function cron_all_bool($v) {
    return in_array(strtolower((string)$v), ['1', 'true', 'yes', 'on'], true);
}

function cron_all_int_param($key, $default, $min, $max) {
    $value = isset($_GET[$key]) ? (int)$_GET[$key] : (int)$default;
    return max((int)$min, min((int)$max, $value));
}

function cron_all_state_path() {
    return __DIR__ . '/cache/cron_all_state.json';
}

function cron_all_read_state() {
    $p = cron_all_state_path();
    if (!is_file($p)) return [];
    $j = json_decode((string)@file_get_contents($p), true);
    return is_array($j) ? $j : [];
}

function cron_all_write_state($state) {
    $p = cron_all_state_path();
    $tmp = $p . '.tmp';
    @file_put_contents($tmp, json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
    @rename($tmp, $p);
}

function cron_all_due($state, $key, $interval, $now) {
    $last = isset($state[$key]) ? (int)$state[$key] : 0;
    return $last <= 0 || ($now - $last) >= max(1, (int)$interval);
}

function cron_all_require_if_exists($path) {
    if (is_file($path)) require_once $path;
}

function cron_all_bootstrap($withLeftHelpers = false) {
    static $basicLoaded = false;
    static $leftLoaded = false;

    $root = __DIR__;
    if (!$basicLoaded) {
        date_default_timezone_set('Asia/Tehran');

        cron_all_require_if_exists($root . '/config_db.php');
        cron_all_require_if_exists($root . '/lib/db_storage.php');
        cron_all_require_if_exists($root . '/app/Bootstrap.php');
        cron_all_require_if_exists($root . '/app/Support/TextFormatting.php');
        cron_all_require_if_exists($root . '/app/Services/JsonCacheService.php');
        cron_all_require_if_exists($root . '/app/Services/ScaleOptimizerService.php');
        cron_all_require_if_exists($root . '/app/Services/AdminToolsService.php');
        cron_all_require_if_exists($root . '/app/Services/SettingsBackupService.php');
        cron_all_require_if_exists($root . '/app/Services/PaymentReportService.php');
        cron_all_require_if_exists($root . '/app/Services/SimpleShopService.php');
        cron_all_require_if_exists($root . '/app/Services/AccountingService.php');

        if (function_exists('bot_scale_bootstrap')) {
            bot_scale_bootstrap();
        }

        if (!defined('TELEGRAM_API_BASE')) define('TELEGRAM_API_BASE', 'https://api.telegram.org/bot');
        if (!defined('BOT_TELEGRAM_CONNECT_TIMEOUT')) define('BOT_TELEGRAM_CONNECT_TIMEOUT', 6);
        if (!defined('BOT_TELEGRAM_TIMEOUT')) define('BOT_TELEGRAM_TIMEOUT', 16);
        if (!defined('BOT_BROADCAST_PER_RUN')) define('BOT_BROADCAST_PER_RUN', 20);
        if (!defined('BOT_BROADCAST_MAX_PER_RUN')) define('BOT_BROADCAST_MAX_PER_RUN', 20);
        if (!defined('BOT_TELEGRAM_BROADCAST_SAFE_RPS')) define('BOT_TELEGRAM_BROADCAST_SAFE_RPS', 20);

        if (!function_exists('bot')) {
            function bot($method, $datas = []) {
                $map = [
                    'sendmessage'=>'sendMessage','sendphoto'=>'sendPhoto','senddocument'=>'sendDocument','forwardmessage'=>'forwardMessage','copymessage'=>'copyMessage',
                    'editmessagetext'=>'editMessageText','editmessagereplymarkup'=>'editMessageReplyMarkup','deletemessage'=>'deleteMessage',
                    'answercallbackquery'=>'answerCallbackQuery','answerinlinequery'=>'answerInlineQuery','answerprecheckoutquery'=>'answerPreCheckoutQuery',
                    'getfile'=>'getFile','getchat'=>'getChat','getchatmember'=>'getChatMember','getchatadministrators'=>'getChatAdministrators','getme'=>'getMe'
                ];
                $method = $map[strtolower((string)$method)] ?? (string)$method;
                if (!is_array($datas)) $datas = [];
                foreach(['reply_markup','prices','shipping_options','errors','results'] as $field){
                    if(isset($datas[$field]) && (is_array($datas[$field]) || is_object($datas[$field]))) $datas[$field] = json_encode($datas[$field], JSON_UNESCAPED_UNICODE);
                }
                if(isset($datas['parse_mode'])){
                    $pm = strtolower((string)$datas['parse_mode']);
                    if($pm === 'markdown' || $pm === 'markdownv2' || (string)$datas['parse_mode'] === 'MarkDown'){
                        foreach(['text','caption'] as $field){ if(isset($datas[$field]) && is_string($datas[$field])) $datas[$field] = preg_replace('/\*\*(.*?)\*\*/su','<b>$1</b>',$datas[$field]); }
                        $datas['parse_mode']='HTML';
                    } elseif($pm === 'html') $datas['parse_mode']='HTML';
                }
                foreach(['disable_web_page_preview','protect_content','show_alert','ok','allow_sending_without_reply'] as $field){
                    if(isset($datas[$field]) && is_bool($datas[$field])) $datas[$field] = $datas[$field] ? 'true' : 'false';
                }
                if (!defined('API_KEY') || API_KEY === '') {
                    return (object)['ok' => false, 'description' => 'API_KEY_not_defined'];
                }

                $url = TELEGRAM_API_BASE . API_KEY . '/' . $method;
                if (function_exists('curl_init')) {
                    $hasFile = false;
                    foreach($datas as $v){ if(class_exists('CURLFile', false) && $v instanceof CURLFile){ $hasFile=true; break; } }
                    $ch = curl_init();
                    curl_setopt_array($ch, [
                        CURLOPT_URL => $url,
                        CURLOPT_RETURNTRANSFER => true,
                        CURLOPT_CONNECTTIMEOUT => defined('BOT_TELEGRAM_CONNECT_TIMEOUT') ? BOT_TELEGRAM_CONNECT_TIMEOUT : 6,
                        CURLOPT_TIMEOUT => defined('BOT_TELEGRAM_TIMEOUT') ? BOT_TELEGRAM_TIMEOUT : 16,
                        CURLOPT_SSL_VERIFYPEER => true,
                        CURLOPT_SSL_VERIFYHOST => 2,
                    ]);
                    if (!empty($datas)) {
                        curl_setopt($ch, CURLOPT_POST, true);
                        curl_setopt($ch, CURLOPT_POSTFIELDS, $hasFile ? $datas : http_build_query($datas));
                    }
                    $raw = curl_exec($ch);
                    $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
                    if ($raw === false) {
                        $err = curl_error($ch);
                        curl_close($ch);
                        if (function_exists('bot_scale_log_api_error')) bot_scale_log_api_error($method, $http, 'CURL: ' . $err);
                        return (object)['ok' => false, 'description' => 'curl_error: ' . $err];
                    }
                    curl_close($ch);
                } else {
                    $ctx = stream_context_create([
                        'http' => [
                            'method' => empty($datas) ? 'GET' : 'POST',
                            'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
                            'content' => empty($datas) ? '' : http_build_query($datas),
                            'timeout' => defined('BOT_TELEGRAM_TIMEOUT') ? BOT_TELEGRAM_TIMEOUT : 16,
                            'ignore_errors' => true,
                        ],
                        'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
                    ]);
                    $raw = @file_get_contents($url, false, $ctx);
                    $http = 0;
                    if ($raw === false) return (object)['ok' => false, 'description' => 'http_error'];
                }

                $obj = json_decode((string)$raw);
                if (!is_object($obj)) {
                    if (function_exists('bot_scale_log_api_error')) bot_scale_log_api_error($method, $http, 'invalid_json: ' . substr((string)$raw, 0, 200));
                    return (object)['ok' => false, 'description' => 'invalid_json'];
                }
                if (isset($obj->ok) && !$obj->ok && function_exists('bot_scale_log_api_error')) {
                    bot_scale_log_api_error($method, $http, (string)$raw);
                }
                return $obj;
            }
        }

        if (!function_exists('bot_data_get_json')) {
            function bot_data_get_json($path, $default = []) {
                $full = $path;
                if ($path !== '' && $path[0] !== '/') $full = __DIR__ . '/' . $path;
                if (!is_file($full)) return $default;
                $data = json_decode((string)@file_get_contents($full), true);
                return is_array($data) ? $data : $default;
            }
        }
        if (!function_exists('bot_data_put_json')) {
            function bot_data_put_json($path, $data) {
                $full = $path;
                if ($path !== '' && $path[0] !== '/') $full = __DIR__ . '/' . $path;
                $dir = dirname($full);
                if ($dir && !is_dir($dir)) @mkdir($dir, 0775, true);
                return @file_put_contents($full, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX) !== false;
            }
        }
        if (!function_exists('bot_data_file_exists')) {
            function bot_data_file_exists($path) {
                $full = $path;
                if ($path !== '' && $path[0] !== '/') $full = __DIR__ . '/' . $path;
                return is_file($full);
            }
        }
        if (!function_exists('bot_data_glob')) {
            function bot_data_glob($pattern) {
                $full = $pattern;
                if ($pattern !== '' && $pattern[0] !== '/') $full = __DIR__ . '/' . $pattern;
                return glob($full) ?: [];
            }
        }
        if (!function_exists('saveJson')) {
            function saveJson($file, $data) {
                if (function_exists('bot_cache_invalidate_for_path')) bot_cache_invalidate_for_path($file);
                if (function_exists('bot_storage_is_managed_path') && function_exists('bot_data_put_json') && bot_storage_is_managed_path($file)) {
                    bot_data_put_json($file, $data);
                    return;
                }
                $full = $file;
                if ($file !== '' && $file[0] !== '/') $full = __DIR__ . '/' . $file;
                $dir = dirname($full);
                if ($dir && !is_dir($dir)) @mkdir($dir, 0775, true);
                @file_put_contents($full, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
            }
        }
        if (!function_exists('SM')) {
            function SM($chatID, $msg, $mode = 'MarkDown', $reply = null, $keyboard = null) {
                return bot('sendMessage', [
                    'chat_id' => $chatID,
                    'text' => $msg,
                    'parse_mode' => $mode,
                    'reply_to_message_id' => $reply,
                    'reply_markup' => $keyboard,
                ]);
            }
        }

        cron_all_require_if_exists($root . '/lib/broadcast_queue.php');
        $basicLoaded = true;
    }

    if ($withLeftHelpers && !$leftLoaded) {
        // Required by the left-check job. These files only define helper functions.
        cron_all_require_if_exists($root . '/app/Services/TextSettingsService.php');
        cron_all_require_if_exists($root . '/app/Services/RewardService.php');
        cron_all_require_if_exists($root . '/app/Legacy/ChannelHelpers.php');
        cron_all_require_if_exists($root . '/app/Legacy/OrderTrackingHelpers.php');
        cron_all_require_if_exists($root . '/app/Legacy/LeftPenaltyHelpers.php');
        $leftLoaded = true;
    }
}

function cron_all_run_broadcast($limit, $maxRuntime, $safeRps) {
    cron_all_bootstrap(false);
    if (!function_exists('bot_broadcast_process')) {
        return ['ok' => false, 'error' => 'broadcast_queue_not_loaded'];
    }
    return bot_broadcast_process($limit, $maxRuntime, $safeRps);
}

function cron_all_run_cache() {
    cron_all_bootstrap(false);
    $result = ['ok' => true, 'jobs' => []];

    if (function_exists('bot_scale_warmup_cache')) {
        $result['jobs']['scale_cache'] = bot_scale_warmup_cache();
    } else {
        $result['jobs']['scale_cache'] = ['ok' => false, 'error' => 'ScaleOptimizerService_not_found'];
        $result['ok'] = false;
    }

    if (function_exists('bot_broadcast_user_ids')) {
        $ids = bot_broadcast_user_ids(false);
        $result['jobs']['broadcast_user_ids'] = ['ok' => true, 'count' => is_array($ids) ? count($ids) : 0];
    }

    return $result;
}

function cron_all_run_left($maxChecks, $notify) {
    cron_all_bootstrap(true);

    global $data_left, $money;
    if (!isset($data_left) || !is_array($data_left)) {
        $data_left = bot_data_get_json('lib/kodam/data-left.json', []);
        if (!is_array($data_left)) $data_left = [];
    }
    if (!isset($money) || trim((string)$money) === '') {
        $money = 'الماس';
    }

    if (!function_exists('bale_manual_check_left_all_orders')) {
        return ['ok' => false, 'error' => 'left_check_helpers_not_loaded'];
    }

    $res = bale_manual_check_left_all_orders($maxChecks, $notify, 'cron');
    $res['ok'] = true;
    return $res;
}



function cron_all_run_backup($forceBackup = false) {
    cron_all_bootstrap(false);
    if ($forceBackup) {
        if (!function_exists('bot_admin_tools_make_backup')) {
            return ['ok' => false, 'error' => 'AdminToolsService_not_loaded'];
        }
        return bot_admin_tools_make_backup(false);
    }
    if (!function_exists('bot_admin_tools_cron_backup')) {
        return ['ok' => false, 'error' => 'AdminToolsService_not_loaded'];
    }
    return bot_admin_tools_cron_backup();
}

function cron_all_format_bytes($bytes) {
    $bytes = (float)$bytes;
    if ($bytes <= 0) return '0 B';
    $units = ['B', 'KB', 'MB', 'GB'];
    $i = 0;
    while ($bytes >= 1024 && $i < count($units) - 1) { $bytes /= 1024; $i++; }
    return rtrim(rtrim(number_format($bytes, 2, '.', ''), '0'), '.') . ' ' . $units[$i];
}

function cron_all_master_admin_ids() {
    global $admin, $main_admins, $master_admins, $super_admins;
    $ids = [];
    foreach ([$admin ?? null, $main_admins ?? null, $master_admins ?? null, $super_admins ?? null] as $src) {
        foreach ((array)$src as $id) {
            $id = preg_replace('/[^0-9]/', '', (string)$id);
            if ($id !== '') $ids[$id] = $id;
        }
    }
    if (!$ids && is_file(__DIR__ . '/lib/kodam/list.json')) {
        $list = json_decode((string)@file_get_contents(__DIR__ . '/lib/kodam/list.json'), true);
        if (is_array($list) && isset($list['admins']) && is_array($list['admins'])) {
            foreach ($list['admins'] as $id) {
                $id = preg_replace('/[^0-9]/', '', (string)$id);
                if ($id !== '') $ids[$id] = $id;
            }
        }
    }
    return array_values($ids);
}

function cron_all_notify_admins($text, $replyMarkup = null) {
    cron_all_bootstrap(false);
    $ids = cron_all_master_admin_ids();
    $sent = 0;
    foreach ($ids as $adminId) {
        $payload = [
            'chat_id' => $adminId,
            'text' => $text,
            'parse_mode' => 'HTML',
            'disable_web_page_preview' => true,
        ];
        if ($replyMarkup !== null) $payload['reply_markup'] = $replyMarkup;
        $res = bot('sendMessage', $payload);
        if (is_object($res) && !empty($res->ok)) $sent++;
    }
    return ['target_count' => count($ids), 'sent' => $sent];
}


function cron_all_send_backup_file_to_admins($file, $caption = '') {
    cron_all_bootstrap(false);
    $file = (string)$file;
    $ids = cron_all_master_admin_ids();
    $sent = 0;
    $failed = 0;
    $errors = [];
    if ($file === '' || !is_file($file)) {
        return ['target_count' => count($ids), 'sent' => 0, 'failed' => count($ids), 'error' => 'backup_file_not_found'];
    }
    foreach ($ids as $adminId) {
        $ok = false;
        try {
            if (function_exists('bot_admin_tools_send_file')) {
                $res = bot_admin_tools_send_file($adminId, $file, $caption);
                if (is_object($res) && !empty($res->ok)) $ok = true;
                elseif ($res === true) $ok = true;
            } elseif (function_exists('curl_init') && class_exists('CURLFile', false)) {
                $mime = 'application/octet-stream';
                if (preg_match('/\.sql\.gz$/i', $file)) $mime = 'application/gzip';
                elseif (preg_match('/\.sql$/i', $file)) $mime = 'application/sql';
                $res = bot('sendDocument', ['chat_id' => $adminId, 'document' => new CURLFile(realpath($file), $mime, basename($file)), 'caption' => $caption]);
                if (is_object($res) && !empty($res->ok)) $ok = true;
            }
        } catch (Throwable $e) {
            $errors[] = $e->getMessage();
        }
        if ($ok) $sent++; else $failed++;
    }
    return ['target_count' => count($ids), 'sent' => $sent, 'failed' => $failed, 'errors' => array_slice(array_values(array_unique($errors)), 0, 3)];
}

function cron_all_nightly_report_text($backup, $left, $link = '') {
    $lines = [];
    $lines[] = '🌙 <b>گزارش اجرای شبانه ربات</b>';
    $lines[] = '🕛 زمان: ' . date('Y/m/d H:i:s');
    $lines[] = '';
    if (!empty($backup['ok'])) {
        $file = (string)($backup['file'] ?? '');
        $lines[] = '💾 <b>بکاپ دیتابیس:</b> انجام شد ✅';
        if ($file !== '') $lines[] = '📄 فایل: <code>' . htmlspecialchars(basename($file), ENT_QUOTES, 'UTF-8') . '</code>';
        if (isset($backup['size'])) $lines[] = '📦 حجم: ' . cron_all_format_bytes((int)$backup['size']);
        $lines[] = '📎 فایل بکاپ به‌صورت سند جداگانه داخل همین چت ارسال می‌شود.';
        if ($link !== '') $lines[] = '🔗 لینک کمکی ۲۴ ساعته: ' . htmlspecialchars($link, ENT_QUOTES, 'UTF-8');
    } else {
        $lines[] = '💾 <b>بکاپ دیتابیس:</b> خطا ❌';
        $lines[] = '⚠️ ' . htmlspecialchars((string)($backup['error'] ?? $backup['reason'] ?? 'نامشخص'), ENT_QUOTES, 'UTF-8');
    }
    $lines[] = '';
    if (!empty($left['disabled'])) {
        $lines[] = '🚪 <b>بررسی لفت‌دهنده‌ها:</b> غیرفعال است.';
    } else {
        $lines[] = '🚪 <b>بررسی لفت‌دهنده‌ها:</b> انجام شد ✅';
        $lines[] = '🔎 بررسی‌شده: <b>' . (int)($left['checked'] ?? 0) . '</b>';
        $lines[] = '📌 اسکن‌شده: <b>' . (int)($left['scanned'] ?? 0) . '</b>';
        $lines[] = '⛔️ جریمه‌شده: <b>' . (int)($left['penalized'] ?? 0) . '</b>';
        $lines[] = '⚠️ خطا/نامطمئن: <b>' . (int)($left['errors'] ?? 0) . '</b>';
        if (!empty($left['cursor']) && is_array($left['cursor'])) {
            $oid = (string)($left['cursor']['order_id'] ?? '');
            $mid = (string)($left['cursor']['member_id'] ?? '');
            if ($oid !== '' || $mid !== '') $lines[] = '📍 ادامه بررسی از: سفارش <code>' . htmlspecialchars($oid, ENT_QUOTES, 'UTF-8') . '</code> / کاربر <code>' . htmlspecialchars($mid, ENT_QUOTES, 'UTF-8') . '</code>';
        }
    }
    return implode("
", $lines);
}


function cron_all_run_accounting_daily($force=false) {
    cron_all_bootstrap(false);
    if (!function_exists('bot_accounting_send_daily_channel')) {
        return ['ok' => false, 'error' => 'AccountingService_not_found'];
    }
    return bot_accounting_send_daily_channel((bool)$force);
}

function cron_all_run_nightly($leftMaxChecks, $leftNotify) {
    $backup = cron_all_run_backup(true);
    $backupLink = '';
    if (!empty($backup['ok']) && !empty($backup['file']) && function_exists('bot_admin_tools_backup_link')) {
        $backupLink = (string)bot_admin_tools_backup_link($backup['file']);
    }
    $left = cron_all_run_left($leftMaxChecks, $leftNotify);
    $accounting = cron_all_run_accounting_daily(false);
    $text = cron_all_nightly_report_text($backup, $left, $backupLink);
    $notify = cron_all_notify_admins($text);
    $fileSend = ['sent'=>0,'target_count'=>0];
    if(!empty($backup['ok']) && !empty($backup['file'])){
        $caption = '💾 بکاپ دیتابیس شبانه - '.date('Y/m/d H:i');
        $fileSend = cron_all_send_backup_file_to_admins((string)$backup['file'], $caption);
    }
    return [
        'ok' => !empty($backup['ok']) && !empty($left['ok']),
        'backup' => $backup,
        'backup_file_sent_to_telegram' => $fileSend,
        'backup_download_link_created' => $backupLink !== '',
        'left' => $left,
        'accounting_daily' => $accounting,
        'admin_notify' => $notify,
    ];
}

function cron_all_append_file_log($task, array $result, $startedTs) {
    $dir = __DIR__ . '/cache';
    if(!is_dir($dir)) @mkdir($dir, 0775, true);
    $row = [
        'time' => date('c'),
        'task' => (string)$task,
        'status' => !empty($result['ok']) ? 'ok' : 'error',
        'started_ts' => (int)$startedTs,
        'finished_ts' => time(),
        'result' => $result,
    ];
    @file_put_contents($dir . '/cron_all.log', json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "
", FILE_APPEND | LOCK_EX);
    if((string)$task === 'nightly'){
        @file_put_contents($dir . '/cron_nightly.log', json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "
", FILE_APPEND | LOCK_EX);
    }
}

function cron_all_record_run($task, $result, $startedTs) {
    cron_all_append_file_log((string)$task, is_array($result) ? $result : ['ok'=>false,'error'=>'non_array_result'], (int)$startedTs);
    if (!function_exists('bot_sql_pdo')) return;
    $pdo = bot_sql_pdo();
    if (!$pdo) return;
    try {
        $pdo->exec("CREATE TABLE IF NOT EXISTS bot_cron_runs (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
            task_name VARCHAR(80) NOT NULL,
            status VARCHAR(30) NOT NULL DEFAULT 'ok',
            result_json LONGTEXT NULL,
            started_ts BIGINT NOT NULL DEFAULT 0,
            finished_ts BIGINT NOT NULL DEFAULT 0,
            created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
            KEY idx_task_created (task_name, created_at),
            KEY idx_status_created (status, created_at)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
        $st=$pdo->prepare('INSERT INTO bot_cron_runs (task_name,status,result_json,started_ts,finished_ts) VALUES (?,?,?,?,?)');
        $st->execute([(string)$task, (!empty($result['ok'])?'ok':'error'), json_encode($result, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT), (int)$startedTs, time()]);
    } catch (Throwable $e) { error_log('cron run log failed: '.$e->getMessage()); }
}

$now = time();
$state = cron_all_read_state();
$force = isset($_GET['force']) && cron_all_bool($_GET['force']);
$taskParam = isset($_GET['task']) ? trim((string)$_GET['task']) : 'auto';
if ($taskParam === '' || $taskParam === 'all') $taskParam = 'auto';
$tasksRequested = $taskParam === 'auto'
    // auto only runs lightweight tasks; nightly and accounting daily must use their own schedule or task=nightly.
    ? ['broadcast', 'cache', 'left']
    : array_values(array_filter(array_map('trim', explode(',', $taskParam))));

$validTasks = ['broadcast', 'cache', 'left', 'backup', 'nightly', 'accounting_daily'];
$tasksRequested = array_values(array_intersect($tasksRequested, $validTasks));

$broadcastInterval = cron_all_int_param('broadcast_every', 60, 1, 86400);
$cacheInterval = cron_all_int_param('cache_every', 300, 60, 86400);
$leftInterval = cron_all_int_param('left_every', 600, 60, 86400);
$backupInterval = cron_all_int_param('backup_every', 86400, 3600, 604800);
$nightlyInterval = cron_all_int_param('nightly_every', 86400, 3600, 604800);
$accountingDailyInterval = cron_all_int_param('accounting_daily_every', 86400, 3600, 604800);

// Safe Telegram profile for shared hosting: every minute, up to 20 requests, max 20 requests/sec inside the run.
// Telegram's public broadcast ceiling is higher, but this profile avoids 429 and server spikes.
$broadcastLimit = cron_all_int_param('broadcast_limit', 20, 1, 20);
$broadcastRuntime = cron_all_int_param('broadcast_runtime', 25, 5, 55);
$broadcastRps = cron_all_int_param('broadcast_rps', 20, 1, 20);
$__leftSettings = [];
$__leftPath = __DIR__ . '/lib/kodam/data-left.json';
if (is_file($__leftPath)) {
    $__leftSettings = json_decode((string)file_get_contents($__leftPath), true);
    if (!is_array($__leftSettings)) $__leftSettings = [];
}
$__leftDefaultChecks = isset($__leftSettings['max_checks']) ? (int)$__leftSettings['max_checks'] : 20;
$leftMaxChecks = cron_all_int_param('left_max_checks', $__leftDefaultChecks, 1, 200);
$leftNotify = !isset($_GET['left_notify']) || cron_all_bool($_GET['left_notify']);

$results = [];
$ran = [];
$skipped = [];
$errors = [];

foreach ($tasksRequested as $task) {
    $interval = $task === 'broadcast' ? $broadcastInterval : ($task === 'cache' ? $cacheInterval : ($task === 'left' ? $leftInterval : ($task === 'backup' ? $backupInterval : ($task === 'accounting_daily' ? $accountingDailyInterval : $nightlyInterval))));
    if (!$force && !cron_all_due($state, $task, $interval, $now)) {
        $skipped[] = $task;
        continue;
    }

    try {
        if ($task === 'broadcast') {
            $results[$task] = cron_all_run_broadcast($broadcastLimit, $broadcastRuntime, $broadcastRps);
        } elseif ($task === 'cache') {
            $results[$task] = cron_all_run_cache();
        } elseif ($task === 'left') {
            $results[$task] = cron_all_run_left($leftMaxChecks, $leftNotify);
        } elseif ($task === 'backup') {
            $results[$task] = cron_all_run_backup($force);
        } elseif ($task === 'nightly') {
            $results[$task] = cron_all_run_nightly($leftMaxChecks, $leftNotify);
        } elseif ($task === 'accounting_daily') {
            $results[$task] = cron_all_run_accounting_daily($force);
        }
        cron_all_record_run($task, $results[$task], $now);
        $ran[] = $task;
        $state[$task] = $now;
    } catch (Throwable $e) {
        $errors[$task] = $e->getMessage();
        $results[$task] = ['ok' => false, 'error' => $e->getMessage()];
        error_log('cron_all_telegram task failed [' . $task . ']: ' . $e->getMessage());
    }
}

$state['updated_at'] = $now;
cron_all_write_state($state);

echo json_encode([
    'ok' => empty($errors),
    'mode' => $taskParam,
    'ran' => $ran,
    'skipped_due_to_interval' => $skipped,
    'intervals_seconds' => [
        'broadcast' => $broadcastInterval,
        'cache' => $cacheInterval,
        'left' => $leftInterval,
        'backup' => $backupInterval,
        'nightly' => $nightlyInterval,
        'accounting_daily' => $accountingDailyInterval,
    ],
    'limits' => [
        'broadcast_limit_per_run' => $broadcastLimit,
        'broadcast_runtime_seconds' => $broadcastRuntime,
        'broadcast_safe_rps' => $broadcastRps,
        'left_max_checks' => $leftMaxChecks,
    ],
    'results' => $results,
    'errors' => $errors,
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
