<?php

namespace WPSN\Core\Queue;

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

defined('ABSPATH') || exit;

/**
 * Queue Auto-Pause
 *
 * Goal: protect fragile/shared hosting from "retry storms".
 *
 * When background jobs repeatedly hit transient overload/rate-limit responses
 * (HTTP 503 / 429), we pause the queue for a short window.
 *
 * This is intentionally conservative:
 * - only triggers on explicit 503/429 signals
 * - respects wp-config forced pause constants
 */
final class QueueAutoPause
{
    public const STATE_OPTION = 'wpsn_queue_autopause_state';

    private Settings $settings;

    private Logger $logger;

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

    public function register(): void
    {
        // Fired by QueueRunner when a job asks to be retried later.
        add_action('wpsn/queue/job_retry', [$this, 'onJobRetry'], 10, 6);

        // Fired by QueueRunner when a job fails hard.
        add_action('wpsn/queue/job_failed', [$this, 'onJobFailed'], 10, 5);
    }

    /**
     * @param int $jobId
     * @param string $jobType
     * @param array<string,mixed> $payload
     * @param array<string,mixed> $job
     * @param string $error
     * @param int $retryIn
     */
    public function onJobRetry(int $jobId, string $jobType, array $payload, array $job, string $error, int $retryIn): void
    {
        $this->maybeRecord($jobType, $error);
    }

    /**
     * @param int $jobId
     * @param string $jobType
     * @param array<string,mixed> $payload
     * @param array<string,mixed> $job
     * @param string $error
     */
    public function onJobFailed(int $jobId, string $jobType, array $payload, array $job, string $error): void
    {
        $this->maybeRecord($jobType, $error);
    }

    private function enabled(): bool
    {
        return (bool) $this->settings->get('performance.queue.autopause.enabled', true);
    }

    private function windowSeconds(): int
    {
        $mins = (int) $this->settings->get('performance.queue.autopause.window_minutes', 10);
        $mins = max(1, min(180, $mins));
        return $mins * MINUTE_IN_SECONDS;
    }

    private function threshold(): int
    {
        $t = (int) $this->settings->get('performance.queue.autopause.threshold', 3);
        return max(2, min(50, $t));
    }

    private function pauseSeconds(): int
    {
        $mins = (int) $this->settings->get('performance.queue.autopause.pause_minutes', 30);
        $mins = max(1, min(7 * 24 * 60, $mins));
        return $mins * MINUTE_IN_SECONDS;
    }

    private function isForcedPause(): bool
    {
        return (defined('WPSN_QUEUE_PAUSE') && (bool) WPSN_QUEUE_PAUSE)
            || (defined('WPSN_QUEUE_PAUSE_UNTIL') && (int) WPSN_QUEUE_PAUSE_UNTIL > time());
    }

    private function maybeRecord(string $jobType, string $error): void
    {
        if (!$this->enabled()) {
            return;
        }

        $jobType = sanitize_key($jobType);
        $error = trim((string) $error);
        if ($error === '') {
            return;
        }

        $status = $this->extractHttpStatus($error);
        if (!in_array($status, [503, 429], true)) {
            return;
        }

        $this->recordEvent($status, $jobType, $error);
    }

    private function extractHttpStatus(string $error): int
    {
        if (preg_match('/\bHTTP\s*([0-9]{3})\b/i', $error, $m)) {
            $code = (int) ($m[1] ?? 0);
            if ($code > 0) {
                return $code;
            }
        }

        $low = strtolower($error);

        // Fast path for our most common signals.
        if (strpos($low, '503') !== false || strpos($low, 'service unavailable') !== false) {
            return 503;
        }
        if (strpos($low, '429') !== false || strpos($low, 'too many requests') !== false) {
            return 429;
        }

        return 0;
    }

    /**
     * @return array<string,mixed>
     */
    public static function getState(): array
    {
        $state = get_option(self::STATE_OPTION);
        return is_array($state) ? $state : [];
    }

    public static function clearState(): void
    {
        delete_option(self::STATE_OPTION);
    }

    private function recordEvent(int $status, string $jobType, string $error): void
    {
        $now = time();
        $window = $this->windowSeconds();

        $state = get_option(self::STATE_OPTION);
        $state = is_array($state) ? $state : [];

        $events = isset($state['events']) && is_array($state['events']) ? $state['events'] : [];
        $events[] = [
            'ts' => $now,
            'status' => (int) $status,
            'job_type' => $jobType,
        ];

        // Keep only recent window.
        $events = array_values(array_filter($events, static function ($e) use ($now, $window): bool {
            if (!is_array($e)) {
                return false;
            }
            $ts = isset($e['ts']) ? (int) $e['ts'] : 0;
            return $ts > 0 && ($now - $ts) <= $window;
        }));

        // Cap memory.
        if (count($events) > 50) {
            $events = array_slice($events, -50);
        }

        $state['events'] = $events;
        $state['last_seen_at'] = $now;
        $state['last_status'] = (int) $status;
        $state['last_job_type'] = $jobType;
        $state['last_error'] = substr($error, 0, 2000);

        // Count the burst.
        $burst = 0;
        foreach ($events as $e) {
            $code = isset($e['status']) ? (int) $e['status'] : 0;
            if (in_array($code, [503, 429], true)) {
                $burst++;
            }
        }
        $state['burst_count'] = $burst;
        $state['burst_window_seconds'] = $window;

        update_option(self::STATE_OPTION, $state, false);

        // Pause if threshold reached.
        if ($burst < $this->threshold()) {
            return;
        }

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

        if (QueuePause::isPaused()) {
            // Already paused; do not extend automatically.
            return;
        }

        $pauseSeconds = $this->pauseSeconds();
        $mins = (int) max(1, round($window / 60));
        $reason = sprintf('auto:%d burst (%d/%dmin)', (int) $status, (int) $burst, $mins);

        try {
            $until = QueuePause::pause($pauseSeconds, $reason);

            $state = get_option(self::STATE_OPTION);
            $state = is_array($state) ? $state : [];
            $state['last_paused_at'] = $now;
            $state['pause_until'] = (int) $until;
            $state['pause_reason'] = $reason;
            $state['pause_count'] = (int) ($state['pause_count'] ?? 0) + 1;
            update_option(self::STATE_OPTION, $state, false);

            /**
             * Fired when the queue is auto-paused due to a burst of transient errors.
             */
            do_action('wpsn/queue/auto_paused', (int) $until, $reason, (int) $burst, (int) $window, (int) $status, $jobType);
        } catch (\Throwable $e) {
            // Never block.
        }
    }
}
