<?php
declare(strict_types=1);

namespace Ekesto\CacheWarmup\Settings\Actions;

use Ekesto\CacheWarmup\Debug;
use Ekesto\CacheWarmup\Integrations\InvalidatorAdapterGate;
use Ekesto\CacheWarmup\WarmupRuntime;
use Ekesto\CacheWarmup\Queue;
use Ekesto\CacheWarmup\Runner;
use Ekesto\CacheWarmup\Settings;
use Ekesto\CacheWarmup\Settings\AdminPage;
use Ekesto\CacheWarmup\Settings\Repository;
use Ekesto\CacheWarmup\Support\Access;

/**
 * Handles manual warmup actions from admin-post and REST.
 */
final class RunNowAction
{
    /**
     * Handle admin-post request for triggering an immediate warmup.
     */
    public static function handleAdminPost(): void
    {
        if (!Access::userCanManage()) {
            wp_die(esc_html__('Not allowed.', 'cache-autopilot'));
        }

        check_admin_referer('ekesto_cache_warmup_run_now');

        set_transient('ekesto_ecw_run_now_notice', '1', 60);

        self::runNextBatchNow();

        $back = AdminPage::settingsPageUrl([
            'tab' => 'logging',
            'ekesto_ecw_run_now' => '1',
        ]);

        wp_safe_redirect($back);
        exit;
    }

    /**
     * Execute one warmup batch immediately in the current request.
     *
     * @return array<string,mixed> Structured execution result.
     */
    public static function runNextBatchNow(): array
    {
        $preflight = self::preflightRunNowRequest();
        if (!$preflight['ready']) {
            return $preflight['result'];
        }

        $before = $preflight['before'];

        Runner::run();

        $after = self::queueSnapshot();

        if ($after['status'] === 'idle') {
            $processed = max(0, $before['total'] - $before['cursor']);

            return self::buildResult(
                'completed',
                __('Processed the final warmup batch. The queue is complete.', 'cache-autopilot'),
                200,
                [
                    'processed_count' => $processed,
                    'queue_remaining' => 0,
                ]
            );
        }

        if ($after['status'] === 'valid') {
            $processed = max(0, $after['cursor'] - $before['cursor']);

            if ($processed > 0) {
                return self::buildResult(
                    'processed',
                    __('Processed the next warmup batch. Remaining batches will continue through WP-Cron.', 'cache-autopilot'),
                    200,
                    [
                        'processed_count' => $processed,
                        'queue_remaining' => $after['remaining'],
                    ]
                );
            }

            if (($before['phase'] ?? '') === 'preparing') {
                return self::buildResult(
                    'prepared',
                    __('Prepared the full warmup queue. Remaining batches will continue through WP-Cron.', 'cache-autopilot'),
                    200,
                    [
                        'processed_count' => 0,
                        'queue_remaining' => $after['remaining'],
                    ]
                );
            }
        }

        if (Queue::isRunLocked()) {
            return self::buildResult(
                'locked',
                __('Another warmup batch is already running.', 'cache-autopilot'),
                409,
                $after['status'] === 'valid' ? $after : $before
            );
        }

        return self::buildResult(
            'invalid_queue',
            __('Warmup queue is invalid or empty. Schedule a warmup first.', 'cache-autopilot'),
            409,
            $after['status'] === 'valid' ? $after : $before
        );
    }

    /**
     * Request an imminent warmup batch through the existing cron scheduler.
     *
     * @return array<string,mixed> Structured request result.
     */
    public static function requestNextBatchSoon(): array
    {
        $preflight = self::preflightRunNowRequest();
        if (!$preflight['ready']) {
            return $preflight['result'];
        }

        $before = $preflight['before'];

        Runner::scheduleSoon();

        $message = ($before['phase'] ?? '') === 'preparing'
            ? __('Warmup batch requested. Queue preparation will start immediately if WP-Cron loopback succeeds.', 'cache-autopilot')
            : __('Warmup batch requested. It will start immediately if WP-Cron loopback succeeds.', 'cache-autopilot');

        return self::buildResult(
            'requested',
            $message,
            200,
            [
                'processed_count' => 0,
                'queue_remaining' => $before['remaining'],
            ]
        );
    }

    /**
     * Backward-compatible wrapper for previous callers.
     *
     * @return array<string,mixed> Structured execution result.
     */
    public static function scheduleImmediateTickAndTrigger(): array
    {
        return self::runNextBatchNow();
    }

    /**
     * Validate manual run-now prerequisites and capture the current queue snapshot.
     *
     * @return array{ready:bool,before:array{status:string,phase:string,cursor:int,total:int,remaining:int},result:array<string,mixed>}
     */
    private static function preflightRunNowRequest(): array
    {
        $emptySnapshot = [
            'status' => 'idle',
            'phase' => 'idle',
            'cursor' => 0,
            'total' => 0,
            'remaining' => 0,
        ];

        if (self::isMaintenanceModeActive()) {
            return [
                'ready' => false,
                'before' => $emptySnapshot,
                'result' => self::buildResult(
                    'maintenance',
                    __('Warmup is temporarily unavailable during maintenance mode.', 'cache-autopilot'),
                    503
                ),
            ];
        }

        if (!Repository::isEnabled('general.enabled')) {
            return [
                'ready' => false,
                'before' => $emptySnapshot,
                'result' => self::buildResult(
                    'disabled',
                    __('Warmup is paused. Enable warmup before running a batch.', 'cache-autopilot'),
                    409
                ),
            ];
        }

        if (!InvalidatorAdapterGate::hasOperationalAdapter([
            'caller' => 'ecw_manual',
            'reason' => 'manual',
        ])) {
            return [
                'ready' => false,
                'before' => $emptySnapshot,
                'result' => self::buildResult(
                    'no_adapter',
                    __('No operational cache adapter available via Cache Invalidator authority. Warmup batch was not started.', 'cache-autopilot'),
                    409
                ),
            ];
        }

        $before = self::queueSnapshot();

        if ($before['status'] === 'idle') {
            return [
                'ready' => false,
                'before' => $before,
                'result' => self::buildResult(
                    'idle',
                    __('No queued warmup batch is ready to run.', 'cache-autopilot'),
                    409
                ),
            ];
        }

        if ($before['status'] === 'invalid') {
            return [
                'ready' => false,
                'before' => $before,
                'result' => self::buildResult(
                    'invalid_queue',
                    __('Warmup queue is invalid or empty. Schedule a warmup first.', 'cache-autopilot'),
                    409,
                    $before
                ),
            ];
        }

        if (Queue::isRunLocked()) {
            return [
                'ready' => false,
                'before' => $before,
                'result' => self::buildResult(
                    'locked',
                    __('Another warmup batch is already running.', 'cache-autopilot'),
                    409,
                    $before
                ),
            ];
        }

        return [
            'ready' => true,
            'before' => $before,
            'result' => [],
        ];
    }

    /**
     * Build a normalized queue snapshot for pre/post execution inspection.
     *
     * @return array{status:string,phase:string,cursor:int,total:int,remaining:int}
     */
    private static function queueSnapshot(): array
    {
        $state = Queue::get();
        $isPreparing = Queue::isPreparingFullState($state);
        $isRunning = Queue::hasActiveRun($state) && (($state['status'] ?? '') === 'running');

        if (!$isPreparing && !$isRunning) {
            return [
                'status' => 'idle',
                'phase' => 'idle',
                'cursor' => 0,
                'total' => 0,
                'remaining' => 0,
            ];
        }

        $urls = $state['urls'] ?? null;
        $runId = isset($state['run_id']) ? trim((string) $state['run_id']) : '';
        $cursor = isset($state['cursor']) ? (int) $state['cursor'] : 0;
        $total = Queue::getTotalUrls($state);

        if ($isPreparing) {
            if ($runId === '') {
                return [
                    'status' => 'invalid',
                    'phase' => 'preparing',
                    'cursor' => 0,
                    'total' => $total,
                    'remaining' => $total,
                ];
            }

            return [
                'status' => 'valid',
                'phase' => 'preparing',
                'cursor' => 0,
                'total' => $total,
                'remaining' => $total,
            ];
        }

        if (!is_array($urls) || $runId === '') {
            return [
                'status' => 'invalid',
                'phase' => 'running',
                'cursor' => max(0, $cursor),
                'total' => is_array($urls) ? count($urls) : $total,
                'remaining' => 0,
            ];
        }

        $total = count($urls);
        // Cursor stores the next index to process, not the last processed index.
        // Allow cursor === total so remaining becomes 0 exactly at queue completion,
        // including the brief state between Runner finishing and Queue::clear().
        if ($total < 1 || $cursor < 0 || $cursor > $total) {
            return [
                'status' => 'invalid',
                'phase' => 'running',
                'cursor' => max(0, $cursor),
                'total' => $total,
                'remaining' => 0,
            ];
        }

        return [
            'status' => 'valid',
            'phase' => 'running',
            'cursor' => $cursor,
            'total' => $total,
            'remaining' => max(0, $total - $cursor),
        ];
    }

    /**
     * Whether WordPress is currently in maintenance mode.
     */
    private static function isMaintenanceModeActive(): bool
    {
        return file_exists(ABSPATH . '.maintenance')
            || (function_exists('wp_is_maintenance_mode') && wp_is_maintenance_mode());
    }

    /**
     * Create a consistent result payload for REST and other callers.
     *
     * @param string $status
     * @param string $message
     * @param int $httpStatus
     * @param array<string,mixed> $extra
     * @return array<string,mixed>
     */
    private static function buildResult(string $status, string $message, int $httpStatus, array $extra = []): array
    {
        $scheduledNext = self::normalizePositiveInt(wp_next_scheduled(Runner::CRON_HOOK));
        // Internal status meanings:
        // - processed: batch executed and queue continues
        // - completed: batch executed and queue finished
        // - locked: runner already executing
        // - idle: nothing queued
        // - disabled: warmup feature disabled
        // - no_adapter: no supported cache adapter available
        // - cache_disabled: selected cache adapter is active but not caching baseline pages
        // - maintenance: maintenance mode active
        // - invalid_queue: corrupt or incomplete queue state
        $queueRemaining = self::normalizeNonNegativeInt($extra['queue_remaining'] ?? null);
        if ($queueRemaining === null) {
            $queueRemaining = self::normalizeNonNegativeInt($extra['remaining'] ?? null);
        }

        $processedCount = self::normalizeNonNegativeInt($extra['processed_count'] ?? null) ?? 0;

        return [
            'success' => $httpStatus >= 200 && $httpStatus < 300,
            'status' => $status,
            'message' => $message,
            'http_status' => $httpStatus,
            'scheduled_next' => $scheduledNext,
            'queue_remaining' => $queueRemaining,
            'processed_count' => $processedCount,
        ];
    }

    /**
     * Normalize an integer-like value into a non-negative int.
     *
     * Accepts ints and digit-only numeric strings.
     *
     * @param mixed $value
     * @return int|null
     */
    private static function normalizeNonNegativeInt($value): ?int
    {
        if (is_int($value)) {
            return max(0, $value);
        }

        if (is_string($value) && ctype_digit($value)) {
            return (int) $value;
        }

        return null;
    }

    /**
     * Normalize an integer-like value into a positive int.
     *
     * @param mixed $value
     * @return int|null
     */
    private static function normalizePositiveInt($value): ?int
    {
        $normalized = self::normalizeNonNegativeInt($value);

        if ($normalized === null || $normalized < 1) {
            return null;
        }

        return $normalized;
    }
}
