<?php

declare(strict_types=1);

namespace SiteFort\Modules\Scanner\Queue;

use SiteFort\Includes\SiteFortKeyManager;
use SiteFort\Modules\Scanner\Repository\TaskQueueRepository;

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

class TaskScheduler {

	private const MIN_DISPATCH_INTERVAL = 2;
	private const STALE_HEARTBEAT = 45;

	private static bool $cron_spawn_registered = false;

	/**
	 * 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 the next scanner 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';
		}
		if ( ! class_exists( WorkerRuntime::class ) ) {
			require_once __DIR__ . '/WorkerRuntime.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;
		}

		$runtime = WorkerRuntime::current( $scan_id );
		if ( WorkerRuntime::is_worker_active( $scan_id ) ) {
			self::schedule_dispatch_fallback( $scan_id, 10 );
			return;
		}

		if ( ! self::claim_dispatch_slot( $scan_id, $runtime ) ) {
			self::schedule_dispatch_fallback( $scan_id, 10 );
			return;
		}

		if ( ! $runtime->uses_http_transport() ) {
			self::schedule_dispatch_fallback( $scan_id, 0 );
			return;
		}

		self::ensure_loopback_manager_loaded();
		$secret = SiteFortKeyManager::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;
		}

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

		$url = add_query_arg( 'action', ScanWorker::AJAX_ACTION, admin_url( 'admin-ajax.php' ) );
		$user_agent_site = str_replace( [ "\r", "\n" ], '', home_url( '/' ) );
		$args = [
			'timeout'   => $runtime->get_ack_timeout_seconds(),
			'blocking'  => true,
			// 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',
			'user-agent' => 'SiteFort-Scanner/1.0; ' . $user_agent_site,
			'headers'   => [
				'Cache-Control'              => 'no-store, no-cache, must-revalidate, max-age=0',
				'Pragma'                     => 'no-cache',
				'X-SiteFort-Worker-Scan'     => $scan_id,
				'X-SiteFort-Worker-Token'    => $token,
				'X-SiteFort-Worker-Timestamp'=> (string) $timestamp,
				'X-SiteFort-Worker-Signature'=> $signature,
			],
			'body'      => [
				'action'         => ScanWorker::AJAX_ACTION,
				'worker_scan_id' => $scan_id,
				'worker_token'   => $token,
				'worker_ts'      => $timestamp,
				'worker_sig'     => $signature,
			],
		];

		self::schedule_dispatch_fallback( $scan_id, 30 );

		$dispatch_result = wp_remote_post( $url, $args );
		if ( is_wp_error( $dispatch_result ) ) {
			WorkerRuntime::mark_http_dispatch_failure( $scan_id, 'http_error' );
			self::log_dispatch_failure( $scan_id, $dispatch_result->get_error_message() );
			self::schedule_dispatch_fallback( $scan_id, 0 );
			return;
		}

		$status_code = (int) wp_remote_retrieve_response_code( $dispatch_result );
		if ( $status_code < 200 || $status_code >= 300 ) {
			WorkerRuntime::mark_http_dispatch_failure( $scan_id, 'bad_status' );
			self::log_dispatch_failure( $scan_id, 'HTTP ' . $status_code . ' response' );
			self::schedule_dispatch_fallback( $scan_id, 0 );
		}
	}

	private static function log_dispatch_failure( string $scan_id, string $reason ): void {
		$throttle_key = 'sitefort_dispatch_fail_logged_' . md5( $scan_id );
		if ( get_transient( $throttle_key ) ) {
			return;
		}
		set_transient( $throttle_key, 1, 5 * MINUTE_IN_SECONDS );

		if ( class_exists( '\\SiteFort\\Modules\\Scanner\\Service\\ScanLogger' ) ) {
			\SiteFort\Modules\Scanner\Service\ScanLogger::log(
				'Worker loopback dispatch failed (' . sanitize_text_field( $reason ) . '). Continuing with the scheduled background worker.',
				3,
				'warning'
			);
		}
	}

	/**
	 * 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;
		}

		self::ensure_liveness_loaded();
		WorkerLiveness::observe( $scan_id );

		$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 ) );
				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 );
	}

	private static function claim_dispatch_slot( string $scan_id, WorkerRuntime $runtime ): bool {
		$now = time();
		$min_interval = max( self::MIN_DISPATCH_INTERVAL, $runtime->get_dispatch_interval_seconds() );
		global $wpdb;
		$table = self::table_identifier( $wpdb->prefix . 'sitefort_scan_queue' );

		// 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 ( task_runner_lease_until <= %d OR task_runner_lease_until = 0 )
				AND ( %d = 0 OR last_dispatch_at <= %d OR last_dispatch_at = 0 )",
				$now,
				$scan_id,
				$now,
				$min_interval,
				$now - $min_interval
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		if ( false === $updated ) {
			return false;
		}

		if ( (int) $updated > 0 ) {
			return true;
		}

		return false;
	}

	public static function clear_wakeups( string $scan_id ): void {
		$scan_id = sanitize_text_field( $scan_id );
		if ( '' === $scan_id ) {
			return;
		}

		$cleared = 0;
		while ( $next = wp_next_scheduled( 'sitefort_scan_worker_fallback', [ $scan_id ] ) ) {
			wp_unschedule_event( $next, 'sitefort_scan_worker_fallback', [ $scan_id ] );
			$cleared++;
			if ( $cleared >= 20 ) {
				break;
			}
		}
	}

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

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

		// Skip spawns while the worker is flagged not responding; events stay
		// scheduled so a working cron (or core's page-load spawn) drains them.
		self::ensure_liveness_loaded();
		if ( 0 === $delay_seconds && function_exists( 'spawn_cron' ) && ! WorkerLiveness::is_tripped() ) {
			self::spawn_cron_request();
		}
	}

	private static function ensure_liveness_loaded(): void {
		if ( ! class_exists( WorkerLiveness::class ) ) {
			require_once __DIR__ . '/WorkerLiveness.php';
		}
	}

	private static function spawn_cron_request(): void {
		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
			if ( self::$cron_spawn_registered ) {
				return;
			}

			self::$cron_spawn_registered = true;
			register_shutdown_function(
				static function (): void {
					self::post_cron_request();
				}
			);
			return;
		}

		self::post_cron_request();
	}

	private static function post_cron_request(): void {
		wp_remote_post(
			site_url( 'wp-cron.php' ),
			[
				'timeout'   => 0.01,
				'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 ),
			]
		);
	}

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

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

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