<?php

declare(strict_types=1);

/**
 * Scan worker entrypoint.
 */

namespace SecurWP\Modules\Scanner\Queue;

use SecurWP\Modules\Cloud\Services\CloudScanner;


use SecurWP\Modules\Scanner\ScanController;
use SecurWP\Includes\SecurWPKeyManager;

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

class ScanWorker {

	/**
	 * @var ScanController|null
	 */
	private static $controller;

	/**
	 * AJAX action name.
	 */
	const AJAX_ACTION = 'securwp_scan_worker';

	/**
	 * Register AJAX handlers for the worker.
	 *
	 * @return void
	 */
	public static function register() {
		add_action( 'wp_ajax_' . self::AJAX_ACTION, [ __CLASS__, 'handle_ajax' ] );
		add_action( 'wp_ajax_nopriv_' . self::AJAX_ACTION, [ __CLASS__, 'handle_ajax' ] );
		add_action( 'securwp_scan_worker_fallback', [ __CLASS__, 'run_fallback' ], 10, 1 );
	}

	/**
	 * Set the scan controller instance.
	 * 
	 * @param ScanController $controller
	 */
	public static function set_controller( ScanController $controller ) {
		self::$controller = $controller;
	}

	/**
	 * Handle async worker dispatch.
	 *
	 * @return void
	 */
	public static function handle_ajax() {
		if ( ! self::verify_loopback_request() ) {
			wp_send_json_error( [ 'message' => 'Unauthorized worker request' ], 403 );
		}

		ignore_user_abort( true );

		$payload = self::get_loopback_payload();
		$scan_id = sanitize_text_field( (string) ( $payload['scan_id'] ?? '' ) );

		if ( function_exists( 'fastcgi_finish_request' ) || function_exists( 'litespeed_finish_request' ) ) {
			self::send_loopback_ack();
			self::run( $scan_id );
			return;
		}

		$result = self::run( $scan_id );
		wp_send_json_success( $result );
	}

	/**
	 * Verify loopback request authenticity.
	 *
	 * @return bool True when the request is valid.
	 */
	private static function verify_loopback_request(): bool {
		self::ensure_loopback_manager_loaded();
		$payload = self::get_loopback_payload();
		$scan_id   = $payload['scan_id'];
		$token     = $payload['token'];
		$timestamp = $payload['timestamp'];
		$signature = $payload['signature'];

		if ( $scan_id === '' || $token === '' || $timestamp <= 0 || $signature === '' ) {
			self::log_loopback_auth_failure( 'missing_payload', $payload['source'] );
			return false;
		}

		// Enforce a narrow request window.
		if ( abs( time() - $timestamp ) > 60 ) {
			self::log_loopback_auth_failure( 'timestamp_out_of_window', $payload['source'] );
			return false;
		}

		// Verify the request signature.
		$secret = SecurWPKeyManager::get_scan_secret( $scan_id, self::AJAX_ACTION );
		if ( $secret === '' ) {
			self::log_loopback_auth_failure( 'secret_missing', $payload['source'] );
			return false;
		}
		$expected_sig = SecurWPKeyManager::build_signature( $scan_id, self::AJAX_ACTION, $token, $timestamp );

		if ( ! hash_equals( $expected_sig, $signature ) ) {
			self::log_loopback_auth_failure( 'signature_mismatch', $payload['source'] );
			return false;
		}

		// Reject replayed signatures within the auth window.
		if ( ! self::mark_loopback_signature_used( $signature ) ) {
			self::log_loopback_auth_failure( 'replay_detected', $payload['source'] );
			return false;
		}

		return true;
	}

	/**
	 * Resolve worker payload from headers or POST.
	 *
	 * @return array{scan_id:string,token:string,timestamp:int,signature:string,source:string}
	 */
	private static function get_loopback_payload(): array {
		$payload = [
			'scan_id'   => '',
			'token'     => '',
			'timestamp' => 0,
			'signature' => '',
			'source'    => '',
		];

			$header_scan_id = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_SECURWP_WORKER_SCAN'] ?? '' ) );
			$header_token = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_SECURWP_WORKER_TOKEN'] ?? '' ) );
			$header_ts = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_SECURWP_WORKER_TIMESTAMP'] ?? '' ) );
			$header_sig = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_SECURWP_WORKER_SIGNATURE'] ?? '' ) );

			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Worker callbacks are authenticated with signed headers/body tokens instead of admin nonces.
			$post_scan_id = sanitize_text_field( wp_unslash( $_POST['worker_scan_id'] ?? '' ) );
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Worker callbacks are authenticated with signed headers/body tokens instead of admin nonces.
			$post_token = sanitize_text_field( wp_unslash( $_POST['worker_token'] ?? '' ) );
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Worker callbacks are authenticated with signed headers/body tokens instead of admin nonces.
			$post_ts = sanitize_text_field( wp_unslash( $_POST['worker_ts'] ?? '' ) );
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Worker callbacks are authenticated with signed headers/body tokens instead of admin nonces.
			$post_sig = sanitize_text_field( wp_unslash( $_POST['worker_sig'] ?? '' ) );

		if ( $header_scan_id && $header_token && $header_ts && $header_sig ) {
			$payload['scan_id'] = $header_scan_id;
			$payload['token'] = $header_token;
			$payload['timestamp'] = intval( $header_ts );
			$payload['signature'] = $header_sig;
			$payload['source'] = 'header';

			if ( $post_scan_id || $post_token || $post_ts || $post_sig ) {
				$header_payload = $header_scan_id . '|' . $header_token . '|' . $header_ts . '|' . $header_sig;
				$post_payload = $post_scan_id . '|' . $post_token . '|' . $post_ts . '|' . $post_sig;
				if ( ! hash_equals( $header_payload, $post_payload ) ) {
					self::log_loopback_auth_failure( 'payload_mismatch', 'header+post' );
					return [
						'scan_id'   => '',
						'token'     => '',
						'timestamp' => 0,
						'signature' => '',
						'source'    => 'header+post',
					];
				}
			}

			return $payload;
		}

		if ( $post_scan_id && $post_token && $post_ts && $post_sig ) {
			$payload['scan_id'] = $post_scan_id;
			$payload['token'] = $post_token;
			$payload['timestamp'] = intval( $post_ts );
			$payload['signature'] = $post_sig;
			$payload['source'] = 'post';
		}

		return $payload;
	}

	/**
	 * Load loopback key manager without relying on autoloaders.
	 *
	 * @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;
		}
	}

	/**
	 * Send a fast loopback ACK to avoid client aborts on async requests.
	 *
	 * @return void
	 */
	private static function send_loopback_ack(): void {
		if ( headers_sent() ) {
			return;
		}

		nocache_headers();
		header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) );
		echo wp_json_encode( [
			'success' => true,
			'data'    => [ 'status' => 'accepted' ],
		] );

		self::finish_request();
	}

	/**
	 * Finish the HTTP response early across common PHP server APIs.
	 *
	 * @return void
	 */
	private static function finish_request(): void {
		if ( function_exists( 'litespeed_finish_request' ) ) {
			@litespeed_finish_request();
			return;
		}

		if ( function_exists( 'fastcgi_finish_request' ) ) {
			@fastcgi_finish_request();
			return;
		}

		if ( function_exists( 'session_write_close' ) ) {
			@session_write_close();
		}

		while ( ob_get_level() > 0 ) {
			@ob_end_flush();
		}
		@flush();
	}

	/**
	 * Prevent replay of authenticated loopback requests.
	 *
	 * @param string $signature Request signature.
	 * @return bool True if the signature is fresh.
	 */
	private static function mark_loopback_signature_used( string $signature ): bool {
		if ( $signature === '' ) {
			return false;
		}

		$key = 'securwp_worker_sig_' . substr( hash( 'sha256', $signature ), 0, 32 );
		if ( get_transient( $key ) ) {
			return false;
		}

		set_transient( $key, 1, 120 );
		return true;
	}

	/**
	 * Throttle auth failure logging to avoid log storms.
	 *
	 * @param string $reason Failure reason.
	 * @param string $source Payload source (header|post|header+post).
	 * @return void
	 */
	private static function log_loopback_auth_failure( string $reason, string $source ): void {
		$throttle_key = 'securwp_worker_auth_fail_log_' . $reason;
		if ( get_transient( $throttle_key ) ) {
			return;
		}

		set_transient( $throttle_key, 1, 60 );

		self::log( sprintf( 'Worker auth rejected (%s). Source=%s.', $reason, $source ?: 'none' ) );
	}
	/**
	 * Run the worker - process one iteration.
	 *
	 * @return array Result of this iteration.
	 */
	public static function run( ?string $scan_id = null ) {
		\SecurWP\Modules\Scanner\Service\ScanLogger::capture_memory_baseline();

		if ( ! class_exists( TaskRunner::class ) ) {
			require_once __DIR__ . '/TaskRunner.php';
		}
		if ( ! class_exists( TaskScheduler::class ) ) {
			require_once __DIR__ . '/TaskScheduler.php';
		}

		$queue = $scan_id ? ScanQueue::get_job( $scan_id ) : ScanQueue::get();
		if ( ! is_array( $queue ) ) {
			return [
				'status'  => 'idle',
				'message' => 'No active scan',
			];
		}

		$scan_id = $scan_id ?: (string) ( $queue['id'] ?? '' );

		$runner = new TaskRunner();
		$result = $runner->run( $scan_id );

		if ( ! empty( $result['available_work'] ) && '' !== $scan_id ) {
			TaskScheduler::dispatch_worker( $scan_id, true );
		} elseif ( ! empty( $result['pending_work'] ) && ! empty( $result['next_available_at'] ) && '' !== $scan_id ) {
			$delay = max( 1, (int) $result['next_available_at'] - time() );
			TaskScheduler::schedule_worker_wakeup( $scan_id, $delay );
		}

		\SecurWP\Modules\Scanner\Service\ScanLogger::capture_memory_end();
		return $result;
	}

	/**
	 * Cron/AS fallback runner.
	 *
	 * @param string $scheduled_scan_id Optional scan ID bound to the scheduled event.
	 * @return array<string,mixed>|void
	 */
	public static function run_fallback( $scheduled_scan_id = '' ) {
		if ( ! ScanQueue::is_active() ) {
			return;
		}

		$queue = ScanQueue::get();
		$active_scan_id = is_array( $queue ) ? (string) ( $queue['id'] ?? '' ) : '';
		$scheduled_scan_id = sanitize_text_field( (string) $scheduled_scan_id );

		// Ignore stale fallback events from older scans.
		if ( $scheduled_scan_id !== '' && $active_scan_id !== '' && ! hash_equals( $active_scan_id, $scheduled_scan_id ) ) {
			return;
		}

		return self::run( $active_scan_id );
	}

	/**
	 * Normalize raw preflight responses into the canonical worker contract.
	 *
	 * @param array|\WP_Error $preflight Raw preflight response.
	 * @return array|\WP_Error Normalized preflight response.
	 */
	public static function normalize_preflight_result( $preflight ) {
		if ( is_wp_error( $preflight ) ) {
			return $preflight;
		}

		$status = (string) ( $preflight['status'] ?? '' );
		if ( $status !== '' && $status !== 'ready' ) {
			$message = (string) ( $preflight['message'] ?? 'Cloud preflight failed.' );
			// Pass through the actual status (e.g. license_invalid) as the error code.
			$code = in_array( $status, [ 'license_invalid', 'unavailable' ], true ) ? $status : 'preflight_failed';
			return new \WP_Error( $code, $message );
		}

		return $preflight;
	}

	/**
	 * Evaluate the full scan-start preflight contract.
	 *
	 * When $persist_side_effects is false, this validates the same gate without
	 * mutating scan-specific runtime state such as signature pinning or cache
	 * metadata persistence.
	 *
	 * @param CloudScanner|null $cloud Cloud scanner instance.
	 * @param string            $scan_id Scan UUID for scan-scoped persistence.
	 * @param bool              $persist_side_effects Whether to persist runtime side effects.
	 * @return array|WP_Error
	 */
	public static function evaluate_preflight( $cloud = null, string $scan_id = '', bool $persist_side_effects = true ) {
		if ( ! $cloud ) {
			if ( ! class_exists( 'SecurWP\\Modules\\Cloud\\Services\\CloudScanner' ) ) {
				require_once dirname( dirname( __DIR__ ) ) . '/Cloud/Services/CloudScanner.php';
			}
			$cloud = new CloudScanner();
		}

		$preflight = self::normalize_preflight_result( $cloud->preflight_check() );
		if ( is_wp_error( $preflight ) ) {
			return $preflight;
		}

		// Store hash cache public key for proof verification (required for cache reuse).
		$hash_cache = $preflight['hash_cache'] ?? null;
		if ( is_array( $hash_cache ) ) {
			$enabled = (bool) ( $hash_cache['enabled'] ?? false );
			if ( ! $enabled ) {
				return new \WP_Error(
					'hash_cache_signing_disabled',
					'Intel Engine hash cache signing is disabled. Enable it to reuse safe/verified cache.'
				);
			}

			$key_id = (int) ( $hash_cache['key_id'] ?? -1 );
			$public_key = (string) ( $hash_cache['public_key'] ?? '' );

			if ( ! class_exists( 'SecurWP\\Modules\\Scanner\\Security\\HashCacheProof' ) ) {
				require_once dirname( __DIR__ ) . '/Security/HashCacheProof.php';
			}
			if ( class_exists( 'SecurWP\\Modules\\Scanner\\Security\\HashCacheProof' ) ) {
				$ok = $persist_side_effects
					? \SecurWP\Modules\Scanner\Security\HashCacheProof::store_public_key( $key_id, $public_key )
					: \SecurWP\Modules\Scanner\Security\HashCacheProof::accepts_public_key( $key_id, $public_key );
				if ( ! $ok ) {
					return new \WP_Error(
						'hash_cache_key_invalid',
						'Intel Engine hash cache public key is invalid or mismatched.'
					);
				}
			}
		}

		$hash_cache_epoch = (string) ( $preflight['hash_cache_epoch'] ?? '' );
		if ( $persist_side_effects && $hash_cache_epoch !== '' ) {
			if ( class_exists( '\SecurWP\\Modules\\Scanner\\Service\\ScannerSettings' ) ) {
				$settings = \SecurWP\Modules\Scanner\Service\ScannerSettings::get_all();
				$prev_epoch = (string) ( $settings['hash_cache_epoch'] ?? '' );
				if ( $prev_epoch !== '' && $prev_epoch !== $hash_cache_epoch ) {
					if ( ! class_exists( '\SecurWP\\Modules\\Scanner\\Repository\\HashCacheRepository' ) ) {
						require_once dirname( __DIR__ ) . '/Repository/HashCacheRepository.php';
					}
					if ( class_exists( '\SecurWP\\Modules\\Scanner\\Repository\\HashCacheRepository' ) ) {
						// Epoch changes are an explicit server signal to drop all cached proofs.
						$repo = new \SecurWP\Modules\Scanner\Repository\HashCacheRepository();
						$deleted = $repo->invalidate_all();
						self::log( sprintf( 'Hash cache epoch updated (%s -> %s). Invalidated %d cache entries.', $prev_epoch, $hash_cache_epoch, $deleted ) );
					}
				}
				\SecurWP\Modules\Scanner\Service\ScannerSettings::set_internal_setting( 'hash_cache_epoch', $hash_cache_epoch );
			}
		}

		$signature_version = null;
		$pinned_version = ( $persist_side_effects && $scan_id !== '' )
			? get_transient( 'securwp_scan_sig_version_' . $scan_id )
			: false;
		if ( $pinned_version ) {
			$signature_version = $pinned_version;
			if ( $persist_side_effects ) {
				self::log( 'Using pinned signature version: ' . $signature_version );
			}
		} else {
			$signature_version = $preflight['signature_version'] ?? null;

			if ( ! $signature_version ) {
				return new \WP_Error(
					'preflight_failed',
					'Failed to retrieve malware signature version from Intel Engine'
				);
			}

			if ( $persist_side_effects && class_exists( '\SecurWP\\Modules\\Scanner\\Service\\ScannerSettings' ) ) {
				$settings = \SecurWP\Modules\Scanner\Service\ScannerSettings::get_all();
				$prev_sig = (string) ( $settings['malware_sig_version'] ?? '' );
				if ( ! empty( $prev_sig ) && $prev_sig !== '0.0.0' && $prev_sig !== $signature_version ) {
					if ( class_exists( '\SecurWP\\Modules\\Scanner\\Repository\\HashCacheRepository' ) ) {
						$repo = new \SecurWP\Modules\Scanner\Repository\HashCacheRepository();
						$deleted = $repo->invalidate_safe();
						self::log( sprintf( 'Signature updated (%s -> %s). Invalidated %d SAFE cache entries.', $prev_sig, $signature_version, $deleted ) );
					}
				}
				\SecurWP\Modules\Scanner\Service\ScannerSettings::set_internal_setting( 'malware_sig_version', $signature_version );
			}

			if ( $persist_side_effects && $scan_id !== '' ) {
				set_transient( 'securwp_scan_sig_version_' . $scan_id, $signature_version, HOUR_IN_SECONDS );

				global $wpdb;
				$table_name = $wpdb->prefix . 'securwp_scan_queue';
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Persists the resolved signature version to the plugin-owned scan queue record.
				$wpdb->update(
					$table_name,
					[ 'malware_signature_version' => $signature_version ],
					[ 'job_id' => $scan_id ],
					[ '%s' ],
					[ '%s' ]
				);
			}
		}

		// Parse license/credit block and persist for the credit widget.
		$license = $preflight['license'] ?? null;
		if ( is_array( $license ) ) {
			$credits_remaining    = isset( $license['credits_remaining'] ) ? (int) $license['credits_remaining'] : -1;
			$credits_total        = isset( $license['credits_total'] )     ? (int) $license['credits_total']     : -1;
			$credit_reset_at      = $license['credit_reset_at'] ?? null;
			$credit_limit_reached = (bool) ( $license['credit_limit_reached'] ?? false );
			$license_plan         = (string) ( $license['type'] ?? 'free' );

			if ( $persist_side_effects ) {
				if ( ! class_exists( '\SecurWP\Modules\Cloud\Service\CloudSettings' ) ) {
					require_once dirname( dirname( __DIR__ ) ) . '/Cloud/Service/CloudSettings.php';
				}
				if ( class_exists( '\SecurWP\Modules\Cloud\Service\CloudSettings' ) ) {
					\SecurWP\Modules\Cloud\Service\CloudSettings::update_stored_fields( [
						'license_plan'              => $license_plan,
						'scan_credits_remaining'    => $credits_remaining,
						'scan_credits_total'        => $credits_total,
						'scan_credit_reset_at'      => $credit_reset_at,
						'scan_credit_limit_reached' => $credit_limit_reached,
					] );
				}
			}

			if ( $credit_limit_reached ) {
				return new \WP_Error(
					'credits_exhausted',
					'Your cloud scan credits have been exhausted for this period. Upgrade your plan to continue scanning.'
				);
			}
		}

		if ( $persist_side_effects ) {
			self::log( 'Preflight check passed. Signature version: ' . $signature_version );
		}

		return [
			'signature_version' => $signature_version,
			'status'            => 'ready',
		];
	}

	/**
	 * Run preflight check to validate environment and get signature version.
	 *
	 * @param string        $scan_id Scan UUID.
	 * @param CloudScanner  $cloud   Cloud scanner instance (optional).
	 * @return array|WP_Error Preflight result or error.
	 */
	public static function run_preflight_check( string $scan_id, $cloud = null ) {
		return self::evaluate_preflight( $cloud, $scan_id, true );
	}

	/**
	 * Dispatch the next worker.
	 *
	 * @return void
	 */
	public static function dispatch_next( ?string $scan_id = null, bool $force = false ) {
		if ( ! class_exists( TaskScheduler::class ) ) {
			require_once __DIR__ . '/TaskScheduler.php';
		}

		TaskScheduler::dispatch_worker( $scan_id, $force );
	}

	/**
	 * Log message to ScanLogger if available.
	 *
	 * @param string $message Log message.
	 * @return void
	 */
	private static function log( string $message ) {
		if ( class_exists( 'SecurWP\\Modules\\Scanner\\Service\\ScanLogger' ) ) {
			\SecurWP\Modules\Scanner\Service\ScanLogger::log( $message );
		}
	}

}
