<?php

/**
 * Broadcast Processor - Designed for shared hosting compatibility
 * 
 * Fixes applied:
 * - Internal timer: stops before hosting kills the script (max 20s)
 * - Progress saved after EVERY message (no lost progress on crash)
 * - Early HTTP response: closes connection immediately, continues in background
 * - Multi-method self-chaining (cURL > fsockopen > file_get_contents)
 * - Shorter API timeouts to prevent single calls from stalling
 * - Proper return value checking (failed API calls counted correctly)
 * - Shutdown handler saves progress even if hosting kills the script
 * - Works without cron jobs, VPS, or background workers
 */

// Prevent abort when client disconnects
ignore_user_abort(true);
@set_time_limit(120);

require_once __DIR__ . '/vendor/autoload.php';

use Bot\Database;
use Bot\Bot;

$config = require __DIR__ . '/config.php';

// Security check
$secret = $config['cron_secret'] ?? null;
$inputKey = $_GET['key'] ?? null;

if (php_sapi_name() !== 'cli' && $inputKey !== $secret) {
    http_response_code(403);
    die("Access Denied");
}

// === EARLY RESPONSE: Close HTTP connection immediately ===
// The script continues processing in the background.
// This is critical for shared hosting where the trigger (cURL/webhook)
// would otherwise wait for the entire batch to complete.
if (php_sapi_name() !== 'cli') {
    // Clear any existing output buffering
    while (ob_get_level()) ob_end_clean();
    
    ob_start();
    echo "OK";
    header('Content-Type: text/plain');
    header('Connection: close');
    header('Content-Length: ' . ob_get_length());
    ob_end_flush();
    flush();
    
    // PHP-FPM: completely finishes the HTTP response
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    }
    // LiteSpeed (Hostinger uses LiteSpeed): equivalent function
    if (function_exists('litespeed_finish_request')) {
        litespeed_finish_request();
    }
}

// === CONFIGURATION ===
$MAX_RUN_TIME = 20;   // seconds - stop WELL before any hosting timeout
$BATCH_FETCH  = 30;   // fetch this many from DB per run
$MSG_DELAY    = 50000; // microseconds between messages (50ms ~ 20 msgs/sec, safe margin for Telegram 30/sec limit)

$startTime = microtime(true);

// Initialize
$db = new Database($config['db_host'], $config['db_user'], $config['db_pass'], $config['db_name']);
$bot = new Bot($config['bot_token'], $db, $config['admins'], $config['owner_id']);
$bot->setBroadcastMode(); // Shorter API timeouts, fewer retries

// Get active broadcast
$broadcast = $db->getActiveBroadcast();
if (!$broadcast) {
    exit;
}

// Acquire lock (prevents duplicate processing)
if (!$db->acquireBroadcastLock($broadcast['id'])) {
    exit;
}

$bcId            = $broadcast['id'];
$targetType      = $broadcast['target_type'];
$lastProcessedId = $broadcast['last_processed_id'] ?? 0;
$sent            = $broadcast['sent_count'];
$failed          = $broadcast['failed_count'];
$totalTargets    = $broadcast['total_targets'];

$text        = $broadcast['message_text'];
$contentType = $broadcast['content_type'];
$fileId      = $broadcast['file_id'];
$buttons     = isset($broadcast['buttons']) ? json_decode($broadcast['buttons'], true) : null;
$replyMarkup = $buttons ? ['inline_keyboard' => $buttons] : null;

// === SHUTDOWN HANDLER: Save progress even if hosting kills the script ===
$shutdownSaved = false;
register_shutdown_function(function() use ($db, $bcId, &$sent, &$failed, &$lastProcessedId, &$shutdownSaved) {
    if ($shutdownSaved) return;
    $shutdownSaved = true;
    try {
        $db->ensureConnection();
        $db->updateBroadcastProgress($bcId, $sent, $failed, $lastProcessedId);
        $db->releaseBroadcastLock($bcId);
    } catch (\Throwable $e) {
        // Can't do much here, but at least we tried
    }
});

// Fetch batch from database
$batch = $db->getBroadcastBatch($targetType, $lastProcessedId, $BATCH_FETCH);

if (empty($batch)) {
    // No more targets - mark as completed
    $shutdownSaved = true;
    $db->completeBroadcast($bcId);
    exit;
}

// === PROCESS MESSAGES WITH TIMER ===
$processedInRun = 0;

foreach ($batch as $row) {
    // TIMER CHECK: Stop if we've been running too long
    $elapsed = microtime(true) - $startTime;
    if ($elapsed >= $MAX_RUN_TIME) {
        break;
    }
    
    // Update cursor BEFORE sending (so crash after send doesn't re-send)
    $lastProcessedId = $row['id'];
    $targetId = ($targetType === 'all_users') ? $row['user_id'] : $row['chat_id'];
    
    // Personalize text
    $personalizedText = $text;
    if ($targetType === 'all_users' && $text) {
        $firstName = $row['first_name'] ?? '';
        $username  = $row['username'] ?? '';
        $personalizedText = str_ireplace(
            ['{name}', '{fullname}', '{username}', '{first_name}'],
            [$firstName, $firstName, $username ? "@$username" : '', $firstName],
            $text
        );
    }
    
    // Send message - check return value properly
    $result = null;
    try {
        switch ($contentType) {
            case 'photo':     $result = $bot->sendPhoto($targetId, $fileId, $personalizedText, $replyMarkup); break;
            case 'video':     $result = $bot->sendVideo($targetId, $fileId, $personalizedText, $replyMarkup); break;
            case 'animation': $result = $bot->sendAnimation($targetId, $fileId, $personalizedText, $replyMarkup); break;
            case 'document':  $result = $bot->sendDocument($targetId, $fileId, $personalizedText, $replyMarkup); break;
            case 'audio':     $result = $bot->sendAudio($targetId, $fileId, $personalizedText, $replyMarkup); break;
            case 'voice':     $result = $bot->sendVoice($targetId, $fileId, $personalizedText, $replyMarkup); break;
            case 'sticker':   $result = $bot->sendSticker($targetId, $fileId); break;
            case 'text':
            default:
                $result = $bot->sendMessage($targetId, $personalizedText, $replyMarkup);
                break;
        }
    } catch (\Throwable $e) {
        $result = null;
        // Rate limit handling - short sleep to avoid burning through the timer
        if (strpos($e->getMessage(), '429') !== false) {
            preg_match('/retry after (\d+)/i', $e->getMessage(), $matches);
            $sleep = isset($matches[1]) ? min((int)$matches[1], 5) : 2;
            sleep($sleep);
        }
    }
    
    // Count result properly: null/error = failed, valid response = success
    if ($result && isset($result['ok']) && $result['ok']) {
        $sent++;
    } else {
        $failed++;
        // Auto-cleanup: if Telegram returned 403 (Forbidden), the bot can no longer
        // message this user (blocked bot / never truly started). Remove them from
        // future broadcasts so they don't keep inflating the failure count.
        if ($targetType === 'all_users' && $bot->getLastErrorCode() === 403) {
            $db->markUserNotStarted($row['user_id']);
        }
    }
    
    $processedInRun++;
    
    // Save progress after EVERY message - bulletproof against crashes
    $db->updateBroadcastProgress($bcId, $sent, $failed, $lastProcessedId);
    
    // Small delay between messages to respect Telegram rate limits
    usleep($MSG_DELAY);
}

// === CHECK COMPLETION ===
$processedCount = $sent + $failed;

// Method 1: Check by count
if ($processedCount >= $totalTargets) {
    $shutdownSaved = true;
    $db->completeBroadcast($bcId);
    exit;
}

// Method 2: Check if there are actually more rows in the database
$nextBatch = $db->getBroadcastBatch($targetType, $lastProcessedId, 1);
if (empty($nextBatch)) {
    $shutdownSaved = true;
    $db->completeBroadcast($bcId);
    exit;
}

// === MORE MESSAGES REMAINING: Release lock and self-chain ===
$shutdownSaved = true; // Prevent shutdown handler from interfering
$db->releaseBroadcastLock($bcId);

// Trigger next batch via HTTP (self-chain)
$siteUrl = $config['site_url'] ?? '';
if ($siteUrl) {
    $chainUrl = rtrim($siteUrl, '/') . '/process_broadcast.php?key=' . urlencode($secret);
    triggerAsyncUrl($chainUrl);
}

exit;


// =============================================================================
// HELPER: Multi-method async URL trigger
// Tries cURL > fsockopen > file_get_contents for maximum hosting compatibility
// =============================================================================
function triggerAsyncUrl(string $url): bool
{
    // Method 1: cURL (most compatible with shared hosting like Hostinger, GoDaddy, etc.)
    if (function_exists('curl_init')) {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 2,
            CURLOPT_CONNECTTIMEOUT => 2,
            CURLOPT_NOSIGNAL       => 1,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_USERAGENT      => 'BroadcastChain/1.0',
        ]);
        @curl_exec($ch);
        curl_close($ch);
        return true;
    }
    
    // Method 2: fsockopen (fire-and-forget)
    $parts  = parse_url($url);
    $host   = $parts['host'] ?? 'localhost';
    $scheme = $parts['scheme'] ?? 'http';
    $port   = $parts['port'] ?? ($scheme === 'https' ? 443 : 80);
    $path   = ($parts['path'] ?? '/') . (isset($parts['query']) ? '?' . $parts['query'] : '');
    
    $fp = @fsockopen(
        ($scheme === 'https' ? 'ssl://' : '') . $host,
        $port, $errno, $errstr, 2
    );
    if ($fp) {
        fwrite($fp, "GET $path HTTP/1.1\r\nHost: $host\r\nConnection: Close\r\n\r\n");
        fclose($fp);
        return true;
    }
    
    // Method 3: file_get_contents with short timeout (blocking fallback)
    $ctx = stream_context_create([
        'http' => ['timeout' => 2, 'ignore_errors' => true],
        'ssl'  => ['verify_peer' => false, 'verify_peer_name' => false]
    ]);
    @file_get_contents($url, false, $ctx);
    return true;
}
