<?php
/**
 * AuctionForge Sync Safety Net v4.0
 *
 * Runs hourly via external cron. Handles:
 * 1. Reset stuck jobs (processing > 10 min)
 * 2. Spawn worker if pending jobs exist and no active worker
 * 3. Detect ended auctions whose webhook was missed (date-passed + API check)
 * 4. Discover newly published auctions from BidSpirit API
 * 5. Hourly delta check: compare API vs local state, sync changed auctions
 * 6. Fix orphaned lot posts missing taxonomy assignment
 * 7. Purge old queue/log data
 */

use auctionforge\inc\sync\SyncQueue;
use auctionforge\inc\sync\SyncLogger;
use auctionforge\inc\model\Auction;

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

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

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

SyncLogger::log('safety', 'Safety cron started', 'info');

// 1. Reset stuck jobs
$resetCount = SyncQueue::resetStuckJobs();
if ($resetCount > 0) {
    SyncLogger::log('safety', "Reset {$resetCount} stuck job(s)", 'warning');
}

// 2. Check queue status and spawn worker if needed
$counts = SyncQueue::getStatusCounts();
if ($counts['pending'] > 0 && !SyncQueue::hasActiveWorker()) {
    SyncLogger::log('safety', "Found {$counts['pending']} pending job(s), spawning worker", 'info');
    SyncQueue::spawnWorker();
}

// 3. Detect auctions that ended but whose webhook was missed.
//    For each locally-active auction whose date has passed, check the BidSpirit
//    API for its real state. If it's ended there, enqueue a day_ended job.
$activeAuctions = Auction::getActiveAuction();
$activeAuctionTerms = [];
if (!empty($activeAuctions) && is_array($activeAuctions)) {
    foreach ($activeAuctions as $term) {
        if ($term instanceof \WP_Term) {
            $activeAuctionTerms[] = $term;
        }
    }
}

$missedEndedCount = 0;
$now = current_time('timestamp');
$endedStates = ['ENDED', 'ARCHIVED', 'POST_AUCTION'];

foreach ($activeAuctionTerms as $term) {
    $termId = $term->term_id;
    $auctionBsId = \auctionforge\inc\helpers\SettingsHelper::getTermOption($termId, 'auction_id');
    if (empty($auctionBsId)) continue;

    // Only check auctions whose last day has passed
    $days = \auctionforge\inc\helpers\SettingsHelper::getTermOption($termId, 'auction_days');
    $latestDate = 0;
    if (is_array($days) && !empty($days)) {
        foreach ($days as $day) {
            if (!empty($day->date)) {
                $dayTs = strtotime($day->date . 'T23:59:59+00:00');
                if ($dayTs > $latestDate) $latestDate = $dayTs;
            }
        }
    } else {
        $auctionDate = \auctionforge\inc\helpers\SettingsHelper::getTermOption($termId, 'auction_date');
        if (!empty($auctionDate)) {
            $dateOnly = substr($auctionDate, 0, 10);
            $latestDate = strtotime($dateOnly . 'T23:59:59+00:00');
        }
    }

    // Skip if auction day hasn't passed yet
    if ($latestDate === 0 || $latestDate > $now) continue;

    // Auction date has passed but local state is still active — check BidSpirit
    $langCode = \auctionforge\inc\helpers\SettingsHelper::getOption('lang', 'en');
    $apiInfo = Auction::getAuctionInfo($auctionBsId, $langCode);

    if (!empty($apiInfo) && isset($apiInfo->state) && in_array($apiInfo->state, $endedStates)) {
        // BidSpirit says it's ended — enqueue day_ended job
        $jobId = SyncQueue::enqueue(
            SyncQueue::TYPE_DAY_ENDED,
            $auctionBsId,
            null,
            3, // Higher priority than safety full_sync
            'safety-ended'
        );
        if ($jobId) {
            $localState = \auctionforge\inc\helpers\SettingsHelper::getTermOption($termId, 'auction_state');
            SyncLogger::log('safety', "Missed webhook: auction {$auctionBsId} ({$term->name}) is {$apiInfo->state} on BidSpirit but {$localState} locally — enqueued day_ended #{$jobId}", 'warning');
            $missedEndedCount++;
        }
    }
}

if ($missedEndedCount > 0) {
    SyncLogger::log('safety', "Detected {$missedEndedCount} auction(s) with missed end-webhook — enqueued day_ended jobs", 'warning');
    if (!SyncQueue::hasActiveWorker()) {
        SyncQueue::spawnWorker();
    }
}

// 4. Discover new auctions from BidSpirit API
//    AuctionList::init() fetches all auctions and creates WP terms for any
//    that don't exist locally yet. This catches newly published auctions
//    even when the publish webhook is blocked (e.g. by Cloudflare).
SyncLogger::log('safety', 'Discovering auctions from BidSpirit API...', 'info');
try {
    $auctionList = new \auctionforge\inc\import\AuctionList();
    $auctionList->init();
    $discoveryInfo = $auctionList->getLoggedInfo();
    if (!empty($discoveryInfo)) {
        foreach ($discoveryInfo as $key => $info) {
            if ($key === 'auctions created|updated' && !empty($info)) {
                SyncLogger::log('safety', 'Auction discovery: ' . count($info) . ' auction(s) created/updated', 'info');
            }
        }
    }
} catch (\Throwable $e) {
    SyncLogger::log('safety', 'Auction discovery failed: ' . $e->getMessage(), 'error');
}

// Re-fetch active auctions — may now include newly discovered ones
$activeAuctions = Auction::getActiveAuction();
$activeAuctionTerms = [];
if (!empty($activeAuctions) && is_array($activeAuctions)) {
    foreach ($activeAuctions as $term) {
        if ($term instanceof \WP_Term) {
            $activeAuctionTerms[] = $term;
        }
    }
}

// 5. Hourly delta check: compare BidSpirit API state against local data
//    Catches any changes missed due to blocked webhooks (e.g. Cloudflare),
//    network issues, or other webhook delivery failures.
//    Only enqueues syncs for auctions that actually have changes.
$langCode = \auctionforge\inc\helpers\SettingsHelper::getOption('lang', 'en');
$deltaEnqueued = 0;

foreach ($activeAuctionTerms as $term) {
    $termId = $term->term_id;
    $auctionBsId = \auctionforge\inc\helpers\SettingsHelper::getTermOption($termId, 'auction_id');
    if (empty($auctionBsId)) continue;

    // Fetch current state from BidSpirit API
    $apiInfo = Auction::getAuctionInfo($auctionBsId, $langCode);
    if (empty($apiInfo)) continue;

    $needsSync = false;
    $reasons = [];

    // Check 1: lastCatalogUpdate timestamp changed
    if (!empty($apiInfo->lastCatalogUpdate)) {
        $localLastUpdate = \auctionforge\inc\helpers\SettingsHelper::getTermOption($termId, 'auction_lastCatalogUpdate');
        if ($localLastUpdate != $apiInfo->lastCatalogUpdate) {
            $needsSync = true;
            $reasons[] = "catalogUpdate: local={$localLastUpdate} vs api={$apiInfo->lastCatalogUpdate}";
        }
    }

    // Check 2: auction state changed (e.g. READY → RUNNING, RUNNING → ENDED)
    if (!empty($apiInfo->state)) {
        $localState = \auctionforge\inc\helpers\SettingsHelper::getTermOption($termId, 'auction_state');
        if ($localState !== $apiInfo->state) {
            $needsSync = true;
            $reasons[] = "state: local={$localState} vs api={$apiInfo->state}";

            // Update the state immediately so it's reflected on the frontend
            update_term_meta($termId, 'auction_state', $apiInfo->state, $localState);
        }
    }

    if ($needsSync) {
        $reasonStr = implode('; ', $reasons);
        $jobId = SyncQueue::enqueue(
            SyncQueue::TYPE_CATALOG_SYNC,
            $auctionBsId,
            null,
            3,
            'safety-delta'
        );
        if ($jobId) {
            SyncLogger::log('safety', "Delta check: auction {$auctionBsId} ({$term->name}) needs sync — {$reasonStr} — enqueued #{$jobId}", 'warning', $auctionBsId, $jobId);
            $deltaEnqueued++;
        }
    }
}

if ($deltaEnqueued > 0) {
    SyncLogger::log('safety', "Delta check: enqueued {$deltaEnqueued} sync(s) for changed auctions", 'warning');
    if (!SyncQueue::hasActiveWorker()) {
        SyncQueue::spawnWorker();
    }
} else {
    SyncLogger::log('safety', "Delta check: all active auctions in sync with BidSpirit API", 'info');
}

// 6. Fix orphaned lot posts (created but missing taxonomy assignment due to killed workers)
$db = \auctionforge\inc\InstanceDB::getInstanceDB();
$metaTable = \auctionforge\inc\model\LotMetaData::getTableName();
$taxName = \auctionforge\inc\controllers\BidspiritLots::TAX_AUCTION_TYPE;
$activeAuctions = Auction::getActiveAuction();
$activeAuctionIds = Auction::getTermFields($activeAuctions, 'auction_id');
$orphansFixed = 0;

foreach ($activeAuctionIds as $auctionBsId) {
    $term = Auction::findAuctionById($auctionBsId);
    if (!$term instanceof \WP_Term) continue;
    $termId = $term->term_id;

    // Find posts that have meta linking them to this auction but no taxonomy assignment
    $orphans = $db->get_col($db->prepare(
        "SELECT DISTINCT m.postId FROM {$metaTable} m
         INNER JOIN {$db->prefix}posts p ON p.ID = m.postId
         LEFT JOIN {$db->prefix}term_relationships tr ON tr.object_id = m.postId AND tr.term_taxonomy_id = %d
         WHERE m.termId = %d AND p.post_type = 'lot' AND tr.object_id IS NULL AND m.postId > 0",
        $termId, $termId
    ));

    if (!empty($orphans)) {
        foreach ($orphans as $postId) {
            wp_set_object_terms((int) $postId, [$termId], $taxName, false);
            $orphansFixed++;
        }
    }
}

if ($orphansFixed > 0) {
    SyncLogger::log('safety', "Fixed {$orphansFixed} orphaned lot post(s) missing taxonomy assignment", 'warning');
}

// 7. Purge old data
$queueRetention = defined('AUCTIONFORGE_SYNC_QUEUE_RETENTION') ? AUCTIONFORGE_SYNC_QUEUE_RETENTION : 30;
$logRetention = defined('AUCTIONFORGE_SYNC_LOG_RETENTION') ? AUCTIONFORGE_SYNC_LOG_RETENTION : 7;

$queuePurged = SyncQueue::purge($queueRetention);
$logPurged = SyncLogger::purge($logRetention);

if ($queuePurged > 0 || $logPurged > 0) {
    SyncLogger::log('safety', "Purged {$queuePurged} old queue rows, {$logPurged} old log rows", 'info');
}

// Summary
SyncLogger::log('safety', sprintf(
    'Safety cron done: %d reset, %d pending, %d processing, %d completed, %d failed',
    $resetCount,
    $counts['pending'],
    $counts['processing'],
    $counts['completed'],
    $counts['failed']
), 'info');
