<?php
namespace auctionforge\inc\sync;

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

/**
 * AuctionForge Safety Sync Runner
 *
 * Encapsulates all safety-net sync logic so it can be called from both
 * the CLI cron (syncSafety.php) and the admin AJAX handler.
 *
 * Steps:
 * 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
 * 4. Discover new auctions from BidSpirit API
 * 5. Hourly delta check: compare API vs local state
 * 6. Fix orphaned lot posts missing taxonomy assignment
 * 7. Purge old queue/log data
 */
class SafetySync
{
    /**
     * Run the full safety sync pipeline.
     *
     * @return array Summary of actions taken
     */
    public static function run()
    {
        $summary = [
            'stuck_reset'    => 0,
            'missed_ended'   => 0,
            'delta_synced'   => 0,
            'orphans_fixed'  => 0,
            'queue_purged'   => 0,
            'log_purged'     => 0,
            'pending'        => 0,
            'processing'     => 0,
            'completed'      => 0,
            'failed'         => 0,
        ];

        SyncLogger::log('safety', 'Safety sync started (manual trigger)', 'info');

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

        // 2. Check queue status and spawn worker if needed
        $counts = SyncQueue::getStatusCounts();
        $summary['pending']    = $counts['pending'];
        $summary['processing'] = $counts['processing'];
        $summary['completed']  = $counts['completed'];
        $summary['failed']     = $counts['failed'];

        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
        $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)) {
                $jobId = SyncQueue::enqueue(
                    SyncQueue::TYPE_DAY_ENDED,
                    $auctionBsId,
                    null,
                    3,
                    '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++;
                }
            }
        }

        $summary['missed_ended'] = $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
        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
        $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;

            $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
            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_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++;
                }
            }
        }

        $summary['delta_synced'] = $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
        $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;

            $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++;
                }
            }
        }

        $summary['orphans_fixed'] = $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);
        $summary['queue_purged'] = $queuePurged;
        $summary['log_purged'] = $logPurged;

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

        // Summary log
        SyncLogger::log('safety', sprintf(
            'Safety sync done: %d reset, %d missed-ended, %d delta-synced, %d orphans-fixed, %d/%d purged',
            $summary['stuck_reset'],
            $summary['missed_ended'],
            $summary['delta_synced'],
            $summary['orphans_fixed'],
            $summary['queue_purged'],
            $summary['log_purged']
        ), 'info');

        return $summary;
    }
}
