<?php
/**
 * AuctionForge Sync Worker v3
 *
 * Background process that claims and processes jobs from the af_sync_queue table.
 * Spawned by webhooks (exec) or by the safety cron.
 *
 * Usage:
 *   php syncWorker.php --site-root=/www/wwwroot/yoursite.com/
 *   php syncWorker.php  (auto-detects site root from crontasks location)
 */

use auctionforge\inc\sync\SyncQueue;
use auctionforge\inc\sync\SyncLogger;
use auctionforge\inc\sync\CatalogSyncWorker;
use auctionforge\inc\sync\ItemUpdateWorker;
use auctionforge\inc\sync\BulkSyncWorker;

if (!empty($_SERVER['DOCUMENT_ROOT'])) {
    die("Access denied — CLI only\n");
}

// Parse --site-root argument or auto-detect
$siteRoot = '';
foreach ($argv ?? [] as $arg) {
    if (strpos($arg, '--site-root=') === 0) {
        $siteRoot = substr($arg, strlen('--site-root='));
    }
}

if (empty($siteRoot)) {
    // Auto-detect: crontasks dir is inside wp-content/plugins/auctionforge/
    $siteRoot = realpath(__DIR__ . '/../../../../') . '/';
}

if (!file_exists($siteRoot . 'wp-load.php')) {
    die("Cannot find WordPress at: {$siteRoot}\n");
}

define('CRON_TASKS_BASE_PATH', __DIR__);
define('WP_USE_THEMES', false);
require($siteRoot . 'wp-load.php');

// Check plugin is active
if (!is_plugin_active(basename(dirname(__DIR__)) . '/' . basename(dirname(__DIR__)) . '.php')) {
    die("Plugin AuctionForge disabled\n");
}

$timeout = defined('AUCTIONFORGE_SYNC_WORKER_TIMEOUT') ? AUCTIONFORGE_SYNC_WORKER_TIMEOUT : 240;
$startTime = time();
$pid = getmypid();
$jobsProcessed = 0;
$jobsFailed = 0;
$GLOBALS['af_current_job_id'] = null;

// SIGTERM graceful-shutdown handler. The kill watchdog escalates to SIGKILL
// after 5s, so we keep this fast: flip the in-flight job to failed (with a
// distinct error message), close the DB, and exit cleanly.
if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
    pcntl_async_signals(true);
    $afSigtermHandler = function ($signo) {
        $jid = isset($GLOBALS['af_current_job_id']) ? (int)$GLOBALS['af_current_job_id'] : 0;
        if ($jid > 0) {
            try {
                SyncQueue::fail($jid, 'Killed by admin (SIGTERM, cleanup OK)');
                SyncLogger::log('queue', "Worker PID " . getmypid() . " caught SIGTERM during job #{$jid} — marked failed", 'warning', null, $jid);
            } catch (\Throwable $t) {
                error_log('AuctionForge SIGTERM handler error: ' . $t->getMessage());
            }
        } else {
            SyncLogger::log('queue', "Worker PID " . getmypid() . " caught SIGTERM (no job in flight)", 'warning');
        }
        global $wpdb;
        if (isset($wpdb) && method_exists($wpdb, 'close')) {
            @$wpdb->close();
        }
        exit(0);
    };
    pcntl_signal(SIGTERM, $afSigtermHandler);
    pcntl_signal(SIGINT,  $afSigtermHandler);
}

// Count pending jobs
$initialCounts = SyncQueue::getStatusCounts();
$pendingCount = $initialCounts['pending'];
SyncLogger::log('system', "═══ Worker started (PID {$pid}) ═══", 'info');
SyncLogger::log('system', "  Queue: {$pendingCount} pending, timeout: {$timeout}s", 'info');

// Main loop: claim and process jobs until timeout or queue empty
$jobNumber = 0;
while ((time() - $startTime) < $timeout) {
    $job = SyncQueue::claimNext($pid);

    if (!$job) {
        SyncLogger::log('system', "Queue empty — all jobs processed", 'info');
        break;
    }

    $jobNumber++;
    $jobStart = microtime(true);
    $remaining = $pendingCount > 0 ? $pendingCount - $jobNumber + 1 : '?';
    $GLOBALS['af_current_job_id'] = $job->id;

    SyncLogger::log('system', "━━━ Job {$jobNumber}/{$pendingCount}: #{$job->id} {$job->job_type} [auction {$job->auction_id}] (source: {$job->source}) ━━━", 'info', $job->auction_id, $job->id);

    try {
        $result = [];

        switch ($job->job_type) {
            case SyncQueue::TYPE_CATALOG_SYNC:
            case SyncQueue::TYPE_FULL_SYNC:
                $result = CatalogSyncWorker::process($job);
                break;

            case SyncQueue::TYPE_ITEM_UPDATE:
                $result = ItemUpdateWorker::process($job);
                break;

            case SyncQueue::TYPE_DAY_ENDED:
                $result = CatalogSyncWorker::processDayEnded($job);
                break;

            case SyncQueue::TYPE_ARCHIVE_SYNC:
                $result = CatalogSyncWorker::process($job);
                break;

            case SyncQueue::TYPE_FULL_SYNC_UPCOMING:
            case SyncQueue::TYPE_FULL_SYNC_PAST:
                $result = BulkSyncWorker::process($job);
                break;

            default:
                SyncLogger::log('worker', "Unknown job type: {$job->job_type}", 'warning', $job->auction_id, $job->id);
                SyncQueue::fail($job->id, "Unknown job type: {$job->job_type}");
                $jobsFailed++;
                continue 2;
        }

        $duration = round(microtime(true) - $jobStart, 2);

        if (!empty($result['errors'])) {
            $errorMsg = implode('; ', array_slice($result['errors'], 0, 3));
            if (!empty($result['lots_processed']) || !empty($result['updated'])) {
                SyncQueue::complete($job->id, "Partial: {$errorMsg} ({$duration}s)");
                SyncLogger::log('system', "Job #{$job->id} completed with warnings ({$duration}s)", 'warning', $job->auction_id, $job->id);
                $jobsProcessed++;
            } else {
                SyncQueue::fail($job->id, $errorMsg);
                SyncLogger::log('system', "Job #{$job->id} failed ({$duration}s): {$errorMsg}", 'error', $job->auction_id, $job->id);
                $jobsFailed++;
            }
        } else {
            $summary = '';
            if (isset($result['lots_processed'])) {
                $summary = "{$result['lots_processed']} lots ({$duration}s)";
            } elseif (isset($result['updated'])) {
                $summary = "Item updated ({$duration}s)";
            } else {
                $summary = "OK ({$duration}s)";
            }
            SyncQueue::complete($job->id, $summary);
            SyncLogger::log('system', "Job #{$job->id} completed: {$summary}", 'success', $job->auction_id, $job->id);
            $jobsProcessed++;
        }

        // Progress summary
        $elapsed = time() - $startTime;
        $remainingJobs = $pendingCount - $jobNumber;
        if ($remainingJobs > 0) {
            $avgTime = $elapsed / $jobNumber;
            $eta = round($remainingJobs * $avgTime);
            SyncLogger::log('system', "  Progress: {$jobNumber}/{$pendingCount} done, ~{$eta}s remaining", 'info');
        }

    } catch (\Exception $e) {
        SyncQueue::fail($job->id, $e->getMessage());
        SyncLogger::log('worker', "Unhandled exception: " . $e->getMessage(), 'error', $job->auction_id, $job->id);
        $jobsFailed++;
    } catch (\Throwable $t) {
        SyncQueue::fail($job->id, $t->getMessage());
        SyncLogger::log('worker', "Fatal error: " . $t->getMessage(), 'error', $job->auction_id, $job->id);
        $jobsFailed++;
    } finally {
        $GLOBALS['af_current_job_id'] = null;
    }

    // Check if we're running low on time
    $elapsed = time() - $startTime;
    $timeLeft = $timeout - $elapsed;
    if ($timeLeft < 30) {
        SyncLogger::log('system', "⚠ Nearing timeout ({$timeLeft}s left) — stopping with {$remainingJobs} jobs remaining", 'warning');
        break;
    }
}

$totalDuration = time() - $startTime;
SyncLogger::log('system', "═══ Worker finished (PID {$pid}): {$jobsProcessed} completed, {$jobsFailed} failed, {$totalDuration}s total ═══", 'info');
