<?php

namespace WPSN\Core\Queue;

use WPSN\Core\Logger;
use WPSN\Core\Settings;

defined('ABSPATH') || exit;

final class QueueKernel
{
    private QueueRunner $runner;
    private Logger $logger;

    private bool $directDirty = false;
    private Settings $settings;

    private const DIRECT_LOCK_OPTION = 'wpsn_queue_direct_lock_until';

    public function __construct(QueueRunner $runner, Logger $logger, Settings $settings)
    {
        $this->runner = $runner;
        $this->logger = $logger;
        $this->settings = $settings;
    }

    public function register(): void
    {
        // DB.
        add_action('init', [QueueInstaller::class, 'maybeInstall'], 1);

        // Cron schedule.
        add_filter('cron_schedules', [$this, 'addSchedules']);
        add_action('wpsn_queue_tick', [$this, 'tick']);

        // Ensure scheduled.
        add_action('init', [$this, 'ensureScheduled'], 5);

        add_filter('wpsn/queue/run_at', [$this, 'filterRunAt'], 10, 3);
        add_action('wpsn/queue/enqueued', [$this, 'onEnqueued'], 10, 4);
        add_action('shutdown', [$this, 'maybeRunDirectWorker'], 0);
    }

    /**
     * @param array<string, array<string, mixed>> $schedules
     * @return array<string, array<string, mixed>>
     */
    public function addSchedules(array $schedules): array
    {
        // Keep old schedule key for backward compatibility.
        $defs = [
            'wpsn_minute' => 60,
            'wpsn_2min'   => 120,
            'wpsn_5min'   => 300,
            'wpsn_10min'  => 600,
            'wpsn_15min'  => 900,
        ];

        foreach ($defs as $key => $interval) {
            if (!isset($schedules[$key])) {
                $mins = (int) round($interval / 60);
                $schedules[$key] = [
                    'interval' => $interval,
                    'display'  => sprintf(
                        /* translators: %d is minutes */
                        _n('Every %d minute', 'Every %d minutes', $mins, 'wp-seo-next'),
                        $mins
                    ),
                ];
            }
        }
        return $schedules;
    }

    private function desiredScheduleKey(): string
    {
        $mins = (int) $this->settings->get('performance.queue.interval_minutes', 1);
        switch ($mins) {
            case 2:
                return 'wpsn_2min';
            case 5:
                return 'wpsn_5min';
            case 10:
                return 'wpsn_10min';
            case 15:
                return 'wpsn_15min';
            case 1:
            default:
                return 'wpsn_minute';
        }
    }

    private function desiredIntervalSeconds(): int
    {
        $key = $this->desiredScheduleKey();
        switch ($key) {
            case 'wpsn_2min':
                return 120;
            case 'wpsn_5min':
                return 300;
            case 'wpsn_10min':
                return 600;
            case 'wpsn_15min':
                return 900;
            case 'wpsn_minute':
            default:
                return 60;
        }
    }

    public function ensureScheduled(): void
    {
        $mode = (string) $this->settings->get('performance.queue.mode', 'auto');
        $mode = $mode !== '' ? $mode : 'auto';

        if (in_array($mode, ['manual', 'direct'], true)) {
            // User opted out from internal scheduling.
            if (function_exists('wp_clear_scheduled_hook')) {
                wp_clear_scheduled_hook('wpsn_queue_tick');
            }
            return;
        }

        $desired = $this->desiredScheduleKey();
        $next = wp_next_scheduled('wpsn_queue_tick');

        // If already scheduled but with a different schedule key, reschedule.
        $ev = wp_get_scheduled_event('wpsn_queue_tick');
        if ($ev && isset($ev->schedule) && is_string($ev->schedule) && $ev->schedule !== '' && $ev->schedule !== $desired) {
            if (function_exists('wp_clear_scheduled_hook')) {
                wp_clear_scheduled_hook('wpsn_queue_tick');
            }
            $next = false;
        }

        if (!$next) {
            $interval = $this->desiredIntervalSeconds();
            wp_schedule_event(time() + $interval, $desired, 'wpsn_queue_tick');
        }
    }

    public function tick(): void
    {
        // Operational pause (e.g. shared hosting incidents / maintenance).
        if (QueuePause::isPaused()) {
            return;
        }

        // Keep each cron tick short.
        $max = (int) $this->settings->get('performance.queue.max_jobs', 5);
        if ($max < 1) {
            $max = 1;
        }
        if ($max > 50) {
            $max = 50;
        }
        $processed = $this->runner->run($max);
        if ($processed > 0) {
            $this->logger->info('Queue tick processed ' . $processed . ' job(s).');
        }

        // Maintenance tasks (throttled): keep the queue table small on large sites.
        QueueMaintenance::maybeGarbageCollect($this->settings, $this->logger);
    }

    public function runNow(int $maxJobs = 10): int
    {
        return $this->runner->run($maxJobs);
    }


    /**
     * Clamp near-future run_at timestamps to "now" in Direct mode.
     *
     * @param int $runAtTimestamp
     * @param string $jobType
     * @param array<string,mixed> $payload
     * @return int
     */
    public function filterRunAt(int $runAtTimestamp, string $jobType, array $payload): int
    {
        $mode = (string) $this->settings->get('performance.queue.mode', 'auto');
        if ($mode !== 'direct') {
            return $runAtTimestamp;
        }

        $now = time();
        $threshold = (int) $this->settings->get('performance.queue.direct.eager_run_at_seconds', 5);
        $threshold = max(0, min(60, $threshold));

        if ($runAtTimestamp > 0 && $runAtTimestamp > $now && ($runAtTimestamp - $now) <= $threshold) {
            return $now;
        }

        return $runAtTimestamp;
    }

    /**
     * Mark that we enqueued a job during this request (so shutdown worker can run).
     *
     * @param int $jobId
     * @param string $jobType
     * @param array<string,mixed> $payload
     * @param int $runAtTimestamp
     */
    public function onEnqueued(int $jobId, string $jobType, array $payload, int $runAtTimestamp): void
    {
        $mode = (string) $this->settings->get('performance.queue.mode', 'auto');
        if ($mode !== 'direct') {
            return;
        }

        $this->directDirty = true;
    }

    /**
     * Direct mode: run the queue worker at the end of normal requests (no WP-Cron tick needed).
     */
    public function maybeRunDirectWorker(): void
    {
        $mode = (string) $this->settings->get('performance.queue.mode', 'auto');
        if ($mode !== 'direct') {
            return;
        }

        if ($this->shouldSkipDirectWorkerForRequest()) {
            return;
        }

        if (QueuePause::isPaused()) {
            return;
        }

        // Only run when we actually enqueued something (or when we already have a backlog in admin).
        $counts = $this->runner->counts();
        $pending = (int) ($counts['pending'] ?? 0);
        $running = (int) ($counts['running'] ?? 0);

        $hasBacklog = ($pending + $running) > 0;

        if (!$this->directDirty && !$hasBacklog) {
            return;
        }

        // Avoid doing heavy work on front-end traffic unless we're in cron/cli.
        $isCron = defined('DOING_CRON') && DOING_CRON;
        $isCli = defined('WP_CLI') && WP_CLI;
        if (!$isCron && !$isCli && !is_admin()) {
            // If we only have a backlog (no enqueue this request), don't touch it on front-end.
            if (!$this->directDirty) {
                return;
            }
        }

        $maxJobs = (int) $this->settings->get('performance.queue.direct.max_jobs', 200);
        $maxJobs = max(1, min(5000, $maxJobs));

        $maxSeconds = (int) $this->settings->get('performance.queue.direct.max_seconds', 20);
        $maxSeconds = max(1, min(180, $maxSeconds));

        // Prevent multiple concurrent direct workers on busy shared hosting.
        // This reduces the risk of piling up CPU usage and causing 503.
        if (!$this->acquireDirectLock($maxSeconds + 10)) {
            return;
        }

        try {

            // Try to flush the response early on FastCGI setups.
            if (function_exists('fastcgi_finish_request')) {
                @fastcgi_finish_request();
            }

            $start = microtime(true);
            $processedTotal = 0;

            while (true) {
                if ($processedTotal >= $maxJobs) {
                    break;
                }
                if ((microtime(true) - $start) >= $maxSeconds) {
                    break;
                }

                $remaining = $maxJobs - $processedTotal;
                $chunk = (int) min(50, max(1, $remaining));

                $processed = $this->runner->run($chunk);
                if ($processed <= 0) {
                    break;
                }
                $processedTotal += $processed;
            }

            if ($processedTotal > 0) {
                $this->logger->info('Direct queue worker processed ' . $processedTotal . ' job(s).');
            }

            QueueMaintenance::maybeGarbageCollect($this->settings, $this->logger);
        } finally {
            $this->releaseDirectLock();
        }
    }


    private function shouldSkipDirectWorkerForRequest(): bool
    {
        $action = isset($_REQUEST['action']) ? sanitize_key((string) wp_unslash($_REQUEST['action'])) : '';
        if ($action === '') {
            return false;
        }

        return in_array($action, ['wpsn_queue_live_tick', 'wpsn_queue_live_status'], true);
    }

    /**
     * Best-effort process lock using an option (atomic add_option).
     */
    private function acquireDirectLock(int $ttlSeconds): bool
    {
        $ttlSeconds = max(5, min(600, (int) $ttlSeconds));
        $now = time();
        $until = $now + $ttlSeconds;

        $existing = get_option(self::DIRECT_LOCK_OPTION);
        if ($existing !== false) {
            $ex = (int) $existing;
            if ($ex > $now) {
                return false;
            }
            // Stale lock.
            delete_option(self::DIRECT_LOCK_OPTION);
        }

        // Atomic.
        $ok = add_option(self::DIRECT_LOCK_OPTION, (string) $until, '', 'no');
        if ($ok) {
            return true;
        }

        // Another request acquired it.
        $ex2 = (int) get_option(self::DIRECT_LOCK_OPTION);
        if ($ex2 > $now) {
            return false;
        }
        // Stale again, try once more.
        delete_option(self::DIRECT_LOCK_OPTION);
        return (bool) add_option(self::DIRECT_LOCK_OPTION, (string) $until, '', 'no');
    }

    private function releaseDirectLock(): void
    {
        delete_option(self::DIRECT_LOCK_OPTION);
    }

}
