<?php

namespace WPSN\Core\Queue;

use WPSN\Core\Logger;

defined('ABSPATH') || exit;

final class QueueRunner
{
    private QueueInterface $queue;
    private Logger $logger;

    private const DEFAULT_MAX_ATTEMPTS = 8;

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

    public function run(int $maxJobs = 5): int
    {
        if (QueuePause::isPaused()) {
            return 0;
        }

        $maxJobs = max(1, min(50, $maxJobs));
        $processed = 0;

        for ($i = 0; $i < $maxJobs; $i++) {
            $job = $this->queue->claimNext();
            if (!$job) {
                break;
            }

            $processed++;

            $jobId = (int) ($job['id'] ?? 0);
            $type = (string) ($job['job_type'] ?? '');
            $payload = isset($job['payload_decoded']) && is_array($job['payload_decoded']) ? $job['payload_decoded'] : [];

            // Safety: prevent infinite retry loops.
            $attempts = (int) ($job['attempts'] ?? 0);
            $maxAttempts = (int) apply_filters('wpsn/queue/max_attempts', self::DEFAULT_MAX_ATTEMPTS, (string) $type, (array) $payload, (array) $job);
            $maxAttempts = max(1, min(50, $maxAttempts));
            if ($attempts > $maxAttempts) {
                $err = 'Max attempts exceeded (' . $attempts . ' > ' . $maxAttempts . ').';
                $this->queue->markFailed($jobId, $err);
                do_action('wpsn/queue/job_failed', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err);
                continue;
            }

            try {
                $result = apply_filters('wpsn_queue_handle_' . $type, null, $payload, $job);

                if (is_wp_error($result)) {
                    $retryIn = $this->retryInForWpError($result, (string) $type, (array) $payload, (array) $job);
                    if ($retryIn > 0) {
                        $err = (string) $result->get_error_message();
                        do_action('wpsn/queue/job_retry', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err, (int) $retryIn);
                        $this->queue->retryLater($jobId, (int) $retryIn, (string) $err);
                        continue;
                    }

                    $err = $result->get_error_message();
                    $this->queue->markFailed($jobId, $err);
                    do_action('wpsn/queue/job_failed', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err);
                    continue;
                }

                if (is_array($result)) {
                    $status = isset($result['status']) ? (string) $result['status'] : '';
                    if ($status === 'retry') {
                        $delay = isset($result['retry_in']) ? (int) $result['retry_in'] : 60;
                        $delay = max(60, min(7 * DAY_IN_SECONDS, $delay));
                        $err = isset($result['error']) ? (string) $result['error'] : '';

                        /**
                         * Fired when a queue job requests a retry (soft failure).
                         *
                         * @param int $jobId
                         * @param string $jobType
                         * @param array<string,mixed> $payload
                         * @param array<string,mixed> $job
                         * @param string $error
                         * @param int $retryIn
                         */
                        do_action('wpsn/queue/job_retry', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err, (int) $delay);
                        $this->queue->retryLater($jobId, $delay, $err);
                        continue;
                    }
                    if ($status === 'failed') {
                        $err = isset($result['error']) ? (string) $result['error'] : 'Job failed';
                        $this->queue->markFailed($jobId, $err);

                        /**
                         * Fired when a queue job fails.
                         *
                         * @param int $jobId
                         * @param string $jobType
                         * @param array<string,mixed> $payload
                         * @param array<string,mixed> $job
                         * @param string $error
                         */
                        do_action('wpsn/queue/job_failed', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err);
                        continue;
                    }
                }

                $this->queue->markDone($jobId);
            } catch (\Throwable $e) {
                $msg = $e->getMessage();
                $this->logger->error('Queue job failed: ' . $type . ' #' . $jobId . ' - ' . $msg);
                $this->queue->markFailed($jobId, $msg);

                /**
                 * Fired when a queue job fails.
                 *
                 * @param int $jobId
                 * @param string $jobType
                 * @param array<string,mixed> $payload
                 * @param array<string,mixed> $job
                 * @param string $error
                 */
                do_action('wpsn/queue/job_failed', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $msg);
            }
        }

        return $processed;
    }

    /**
     * Run only jobs of specific types.
     *
     * This is primarily used for admin-ajax driven workflows (e.g. Audit scan),
     * where running unrelated heavy jobs can overload the server and cause 503.
     *
     * @param array<int,string> $jobTypes
     */
    public function runOnlyTypes(array $jobTypes, int $maxJobs = 5): int
    {
        if (QueuePause::isPaused()) {
            return 0;
        }

        $jobTypes = is_array($jobTypes) ? $jobTypes : [];
        $jobTypes = array_values(array_filter(array_map('sanitize_key', $jobTypes), static fn($v) => $v !== ''));
        if (empty($jobTypes)) {
            return $this->run($maxJobs);
        }

        $maxJobs = max(1, min(50, $maxJobs));
        $processed = 0;

        for ($i = 0; $i < $maxJobs; $i++) {
            $job = $this->queue->claimNextByTypes($jobTypes);
            if (!$job) {
                break;
            }

            $processed++;

            $jobId = (int) ($job['id'] ?? 0);
            $type = (string) ($job['job_type'] ?? '');
            $payload = isset($job['payload_decoded']) && is_array($job['payload_decoded']) ? $job['payload_decoded'] : [];

            // Safety: prevent infinite retry loops.
            $attempts = (int) ($job['attempts'] ?? 0);
            $maxAttempts = (int) apply_filters('wpsn/queue/max_attempts', self::DEFAULT_MAX_ATTEMPTS, (string) $type, (array) $payload, (array) $job);
            $maxAttempts = max(1, min(50, $maxAttempts));
            if ($attempts > $maxAttempts) {
                $err = 'Max attempts exceeded (' . $attempts . ' > ' . $maxAttempts . ').';
                $this->queue->markFailed($jobId, $err);
                do_action('wpsn/queue/job_failed', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err);
                continue;
            }

            try {
                $result = apply_filters('wpsn_queue_handle_' . $type, null, $payload, $job);

                if (is_wp_error($result)) {
                    $retryIn = $this->retryInForWpError($result, (string) $type, (array) $payload, (array) $job);
                    if ($retryIn > 0) {
                        $err = (string) $result->get_error_message();
                        do_action('wpsn/queue/job_retry', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err, (int) $retryIn);
                        $this->queue->retryLater($jobId, (int) $retryIn, (string) $err);
                        continue;
                    }

                    $err = $result->get_error_message();
                    $this->queue->markFailed($jobId, $err);
                    do_action('wpsn/queue/job_failed', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err);
                    continue;
                }

                if (is_array($result)) {
                    $status = isset($result['status']) ? (string) $result['status'] : '';
                    if ($status === 'retry') {
                        $delay = isset($result['retry_in']) ? (int) $result['retry_in'] : 60;
                        $delay = max(60, min(7 * DAY_IN_SECONDS, $delay));
                        $err = isset($result['error']) ? (string) $result['error'] : '';

                        /**
                         * Fired when a queue job requests a retry (soft failure).
                         *
                         * @param int $jobId
                         * @param string $jobType
                         * @param array<string,mixed> $payload
                         * @param array<string,mixed> $job
                         * @param string $error
                         * @param int $retryIn
                         */
                        do_action('wpsn/queue/job_retry', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err, (int) $delay);
                        $this->queue->retryLater($jobId, $delay, $err);
                        continue;
                    }
                    if ($status === 'failed') {
                        $err = isset($result['error']) ? (string) $result['error'] : 'Job failed';
                        $this->queue->markFailed($jobId, $err);
                        do_action('wpsn/queue/job_failed', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $err);
                        continue;
                    }
                }

                $this->queue->markDone($jobId);
            } catch (\Throwable $e) {
                $msg = $e->getMessage();
                $this->logger->error('Queue job failed: ' . $type . ' #' . $jobId . ' - ' . $msg);
                $this->queue->markFailed($jobId, $msg);
                do_action('wpsn/queue/job_failed', (int) $jobId, (string) $type, (array) $payload, (array) $job, (string) $msg);
            }
        }

        return $processed;
    }

    public function counts(): array
    {
        return $this->queue->counts();
    }

    /**
     * Decide whether a WP_Error result should be retried.
     */
    private function retryInForWpError(\WP_Error $err, string $jobType, array $payload, array $job): int
    {
        $code = (string) $err->get_error_code();
        $msg = (string) $err->get_error_message();

        $retryIn = 0;

        // Circuit breaker: fail fast and reschedule later.
        if ($code === 'wpsn_http_circuit_open') {
            $data = $err->get_error_data();
            $ra = 0;
            if (is_array($data) && isset($data['retry_after'])) {
                $ra = (int) $data['retry_after'];
            }
            $retryIn = $ra > 0 ? $ra : 60;
        }

        // Network-ish errors (best-effort) can be retried.
        if ($retryIn <= 0) {
            $retryableCodes = [
                'http_request_failed',
                'connect',
                'resolve',
                'timeout',
                'operation_timedout',
                'couldnt_connect',
                'couldnt_resolve_host',
                'couldnt_resolve_proxy',
                'ssl_connect_error',
            ];
            if (in_array($code, $retryableCodes, true)) {
                $retryIn = 120;
            } else {
                $m = strtolower($msg);
                if ($m !== '' && (strpos($m, 'timed out') !== false || strpos($m, 'timeout') !== false)) {
                    $retryIn = 120;
                }
            }
        }

        /**
         * Filter retry delay for WP_Error results.
         *
         * Return 0 to keep current behavior (mark failed).
         *
         * @param int $retryInSeconds
         * @param \WP_Error $err
         * @param string $jobType
         * @param array<string,mixed> $payload
         * @param array<string,mixed> $job
         */
        $retryIn = (int) apply_filters('wpsn/queue/wp_error_retry_in', (int) $retryIn, $err, (string) $jobType, (array) $payload, (array) $job);

        if ($retryIn <= 0) {
            return 0;
        }

        // Clamp.
        $retryIn = max(10, min((int) (7 * DAY_IN_SECONDS), $retryIn));
        return $retryIn;
    }
}
