<?php

declare(strict_types=1);

namespace SecurWP\Modules\Scanner\Queue;

use SecurWP\Includes\SecurWPKeyManager;
use SecurWP\Modules\Scanner\Repository\TaskQueueRepository;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Fire-and-forget wake-up scheduler for the durable scanner task queue.
 */
class TaskScheduler {

	private const MIN_DISPATCH_INTERVAL = 2;
	private const STALE_HEARTBEAT = 30;

	/**
	 * Escape a plugin-owned table identifier for direct interpolation.
	 */
	private static function table_identifier( string $table ): string {
		if ( $table === '' || strlen( $table ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
			throw new \InvalidArgumentException( 'Invalid table identifier.' );
		}

		return '`' . str_replace( '`', '``', $table ) . '`';
	}

	/**
	 * Dispatch a signed loopback worker for the scan.
	 *
	 * @param string|null $scan_id
	 * @param bool        $force
	 * @return void
	 */
	public static function dispatch_worker( ?string $scan_id = null, bool $force = false ): void {
		if ( ! class_exists( TaskQueueRepository::class ) ) {
			require_once dirname( __DIR__ ) . '/Repository/TaskQueueRepository.php';
		}

		$queue = $scan_id ? ScanQueue::get_job( $scan_id ) : ScanQueue::get();
		if ( ! is_array( $queue ) || ! in_array( (string) ( $queue['status'] ?? '' ), [ 'pending', 'running' ], true ) ) {
			return;
		}

		$scan_id = $scan_id ?: (string) ( $queue['id'] ?? '' );
		if ( '' === $scan_id ) {
			return;
		}

		$repo = new TaskQueueRepository();
		if ( ! $force && ! $repo->has_available_work( $scan_id ) ) {
			return;
		}

		self::ensure_loopback_manager_loaded();
		$secret = SecurWPKeyManager::get_scan_secret( $scan_id, ScanWorker::AJAX_ACTION );
		if ( '' === $secret ) {
			ScanQueue::fail(
				'Scanner dispatch key is unavailable. Please restart the scan.',
				[
					'error_code'        => 'dispatch_key_missing',
					'stage'             => 'dispatch',
					'retryable'         => true,
					'status_code'       => 500,
					'technical_message' => 'Missing scan loopback secret for task scheduler dispatch',
				]
			);
			ScanQueue::cleanup_task_queue( $scan_id );
			return;
		}

		$now = time();
		global $wpdb;
		$table = self::table_identifier( $wpdb->prefix . 'securwp_scan_queue' );
		if ( $force ) {
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$updated = $wpdb->query(
				$wpdb->prepare(
					"UPDATE {$table}
					SET last_dispatch_at = %d
					WHERE job_id = %s
					AND status IN ('pending', 'running')",
					$now,
					$scan_id
				)
			);
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			if ( $updated === false ) {
				self::schedule_dispatch_fallback( $scan_id, 1 );
				return;
			}
		} else {
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$updated = $wpdb->query(
				$wpdb->prepare(
					"UPDATE {$table}
					SET last_dispatch_at = %d
					WHERE job_id = %s
					AND status IN ('pending', 'running')
					AND ( last_dispatch_at <= %d OR last_dispatch_at = 0 )",
					$now,
					$scan_id,
					$now - self::MIN_DISPATCH_INTERVAL
				)
			);
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

			if ( (int) $updated <= 0 ) {
				return;
			}
		}

		$token = wp_generate_password( 32, false );
		$timestamp = time();
		$signature = SecurWPKeyManager::build_signature( $scan_id, ScanWorker::AJAX_ACTION, $token, $timestamp );

		$url = add_query_arg( 'action', ScanWorker::AJAX_ACTION, admin_url( 'admin-ajax.php' ) );
		$args = [
			'timeout'   => 1,
			'blocking'  => false,
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- https_local_ssl_verify is a core WordPress HTTP filter.
			'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
			'method'    => 'POST',
			'headers'   => [
				'Cache-Control'              => 'no-store, no-cache, must-revalidate, max-age=0',
				'Pragma'                     => 'no-cache',
				'X-SecurWP-Worker-Scan'     => $scan_id,
				'X-SecurWP-Worker-Token'    => $token,
				'X-SecurWP-Worker-Timestamp'=> (string) $timestamp,
				'X-SecurWP-Worker-Signature'=> $signature,
			],
			'body'      => [
				'action'         => ScanWorker::AJAX_ACTION,
				'worker_scan_id' => $scan_id,
				'worker_token'   => $token,
				'worker_ts'      => $timestamp,
				'worker_sig'     => $signature,
			],
		];

		$dispatch_result = wp_remote_post( $url, $args );
		$fallback_enqueued = false;

		if ( is_wp_error( $dispatch_result ) ) {
			self::schedule_dispatch_fallback( $scan_id, 20 );
			$fallback_enqueued = true;
		}

		if ( ! $fallback_enqueued ) {
			self::schedule_dispatch_fallback( $scan_id, 30 );
		}
	}

	/**
	 * Dispatch a wake-up when work is due or the queue needs reconciliation.
	 *
	 * @param string $scan_id
	 * @return int
	 */
	public static function ensure_progress( string $scan_id ): int {
		if ( ! class_exists( TaskQueueRepository::class ) ) {
			require_once dirname( __DIR__ ) . '/Repository/TaskQueueRepository.php';
		}

		$queue = ScanQueue::get_job( $scan_id );
		if ( ! is_array( $queue ) || ! in_array( (string) ( $queue['status'] ?? '' ), [ 'pending', 'running' ], true ) ) {
			return 0;
		}

		$repo = new TaskQueueRepository();
		$repo->release_expired_leases( $scan_id );

		$now = time();
		$has_available = $repo->has_available_work( $scan_id );
		if ( $has_available ) {
			self::dispatch_worker( $scan_id, true );
			return 1;
		}

		$has_pending = $repo->has_pending_work( $scan_id );
		if ( $has_pending ) {
			$next_available_at = $repo->get_next_available_at( $scan_id );
			if ( $next_available_at > $now ) {
				self::schedule_dispatch_fallback( $scan_id, max( 1, $next_available_at - $now ), false );
				return 0;
			}

			self::dispatch_worker( $scan_id, true );
			return 1;
		}

		$last_ping = (int) ( $queue['last_ping'] ?? 0 );
		if ( $last_ping > 0 && ( $now - $last_ping ) < self::STALE_HEARTBEAT ) {
			return 0;
		}

		self::dispatch_worker( $scan_id, true );
		return 1;
	}

	/**
	 * Schedule a wake-up for future-dated work.
	 *
	 * @param string $scan_id
	 * @param int    $delay_seconds
	 * @return void
	 */
	public static function schedule_worker_wakeup( string $scan_id, int $delay_seconds ): void {
		self::schedule_dispatch_fallback( $scan_id, $delay_seconds );
	}

	/**
	 * @param string $scan_id
	 * @param int    $delay_seconds
	 * @param bool   $allow_async
	 * @return void
	 */
	private static function schedule_dispatch_fallback( string $scan_id, int $delay_seconds = 20, bool $allow_async = true ): void {
		$delay_seconds = max( 1, $delay_seconds );
		$run_at = time() + $delay_seconds;

		$next = wp_next_scheduled( 'securwp_scan_worker_fallback', [ $scan_id ] );
		if ( ! $next || $next > $run_at ) {
			if ( $next ) {
				wp_unschedule_event( $next, 'securwp_scan_worker_fallback', [ $scan_id ] );
			}
			wp_schedule_single_event( $run_at, 'securwp_scan_worker_fallback', [ $scan_id ] );
		}

		if ( $allow_async && $delay_seconds <= 1 && function_exists( 'as_enqueue_async_action' ) ) {
			as_enqueue_async_action( 'securwp_scan_worker_fallback', [ $scan_id ], 'securwp_scanner' );
		} elseif ( function_exists( 'as_schedule_single_action' ) ) {
			$next_async = function_exists( 'as_next_scheduled_action' )
				? (int) as_next_scheduled_action( 'securwp_scan_worker_fallback', [ $scan_id ], 'securwp_scanner' )
				: 0;

			if ( ! $next_async || $next_async > $run_at ) {
				if ( $next_async && function_exists( 'as_unschedule_action' ) ) {
					as_unschedule_action( 'securwp_scan_worker_fallback', [ $scan_id ], 'securwp_scanner' );
				}

				as_schedule_single_action( $run_at, 'securwp_scan_worker_fallback', [ $scan_id ], 'securwp_scanner' );
			}
		}

		if ( function_exists( 'spawn_cron' ) ) {
			spawn_cron( time() );
		}
	}

	/**
	 * @return void
	 */
	private static function ensure_loopback_manager_loaded(): void {
		if ( class_exists( SecurWPKeyManager::class ) ) {
			return;
		}

		$includes = dirname( __DIR__, 3 ) . '/includes/';
		$master_path = $includes . 'MasterKey.php';
		if ( file_exists( $master_path ) ) {
			require_once $master_path;
		}

		$path = $includes . 'SecurWPKeyManager.php';
		if ( file_exists( $path ) ) {
			require_once $path;
		}
	}
}
