<?php
/**
 * Job Health Monitor - Lightweight health monitoring for Action Scheduler reliability.
 *
 * Detects stale jobs and spawns loopback requests to ensure background jobs execute
 * within 30-60 seconds, complementing Action Scheduler's built-in mechanisms.
 *
 * @package AgenticWP
 */

namespace Agentic_WP;

defined( 'ABSPATH' ) || exit;

/**
 * Job Health Monitor class.
 */
final class Job_Health_Monitor {

	/**
	 * Action Scheduler group for AgenticWP jobs.
	 */
	private const GROUP = 'agenticwp_jobs';

	/**
	 * Default threshold in seconds for considering a job stale.
	 */
	private const STALE_THRESHOLD = 120;

	/**
	 * Cooldown in seconds between loopback spawns.
	 */
	private const LOOPBACK_COOLDOWN = 10;

	/**
	 * Transient key for health metrics.
	 */
	private const METRICS_TRANSIENT = 'agenticwp_health_metrics';

	/**
	 * Transient key for pending jobs flag.
	 */
	private const PENDING_JOBS_TRANSIENT = 'agenticwp_has_pending_jobs';

	/**
	 * Transient key for loopback cooldown.
	 */
	private const COOLDOWN_TRANSIENT = 'agenticwp_loopback_cooldown';

	/**
	 * Register hooks for the health monitor.
	 */
	public static function register(): void {
		add_action( 'wp_ajax_agenticwp_spawn_loopback', array( __CLASS__, 'handle_spawn_loopback_ajax' ) );
		add_action( 'wp_ajax_nopriv_agenticwp_spawn_loopback', array( __CLASS__, 'handle_spawn_loopback_ajax' ) );

		// Heartbeat health monitoring.
		add_filter( 'heartbeat_received', array( __CLASS__, 'handle_heartbeat_received' ), 10, 2 );
		add_filter( 'heartbeat_settings', array( __CLASS__, 'configure_heartbeat_settings' ) );

		// Frontend beacon trigger.
		add_action( 'wp_footer', array( __CLASS__, 'inject_frontend_beacon' ) );
	}

	/**
	 * Handle heartbeat received event for health monitoring.
	 *
	 * Checks for stale jobs and spawns loopback when needed.
	 *
	 * @param array $response The heartbeat response data.
	 * @param array $data     The heartbeat request data.
	 * @return array Modified response with health status.
	 */
	public static function handle_heartbeat_received( array $response, array $data ): array {
		if ( empty( $data['agenticwp_health_check'] ) ) {
			return $response;
		}

		$pending = self::has_pending_jobs();
		$stale   = self::get_stale_job_count( self::STALE_THRESHOLD );

		$response['agenticwp_health'] = array(
			'pending' => $pending,
			'stale'   => $stale,
		);

		// Spawn loopback if jobs are stale.
		if ( $stale > 0 ) {
			$spawned                               = self::spawn_as_loopback( 'heartbeat' );
			$response['agenticwp_boost_triggered'] = $spawned;
		}

		return $response;
	}

	/**
	 * Configure heartbeat interval for admin pages.
	 *
	 * Sets 30-second interval on admin pages to ensure timely health checks.
	 *
	 * @param array $settings Heartbeat settings.
	 * @return array Modified settings.
	 */
	public static function configure_heartbeat_settings( array $settings ): array {
		if ( is_admin() ) {
			$settings['interval'] = 30;
		}

		return $settings;
	}

	/**
	 * Check if there are pending AgenticWP jobs.
	 *
	 * @return bool True if pending jobs exist.
	 */
	public static function has_pending_jobs(): bool {
		if ( ! function_exists( 'as_get_scheduled_actions' ) ) {
			return false;
		}

		$args = array(
			'status'   => \ActionScheduler_Store::STATUS_PENDING,
			'group'    => self::GROUP,
			'per_page' => 1,
		);

		$actions = as_get_scheduled_actions( $args );

		return ! empty( $actions );
	}

	/**
	 * Count jobs that have been pending longer than threshold.
	 *
	 * @param int $threshold_seconds Seconds after which a job is considered stale.
	 * @return int Number of stale jobs.
	 */
	public static function get_stale_job_count( int $threshold_seconds = self::STALE_THRESHOLD ): int {
		if ( ! function_exists( 'as_get_scheduled_actions' ) ) {
			return 0;
		}

		$cutoff = time() - $threshold_seconds;

		$args = array(
			'status'       => \ActionScheduler_Store::STATUS_PENDING,
			'group'        => self::GROUP,
			'date'         => gmdate( 'Y-m-d H:i:s', $cutoff ),
			'date_compare' => '<',
			'per_page'     => 100,
		);

		$actions = as_get_scheduled_actions( $args );

		return is_array( $actions ) ? count( $actions ) : 0;
	}

	/**
	 * Spawn an async loopback request to trigger Action Scheduler processing.
	 *
	 * @param string $source Trigger source for metrics tracking (heartbeat, piggyback, frontend).
	 * @return bool True if loopback was spawned, false if on cooldown.
	 */
	public static function spawn_as_loopback( string $source = 'manual' ): bool {
		// Check cooldown to prevent spam.
		if ( get_transient( self::COOLDOWN_TRANSIENT ) ) {
			return false;
		}
		set_transient( self::COOLDOWN_TRANSIENT, true, self::LOOPBACK_COOLDOWN );

		// Trigger Action Scheduler's queue run directly.
		// This is the same approach AS uses internally in ActionScheduler_AsyncRequest_QueueRunner::handle().
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Action Scheduler hook.
		do_action( 'action_scheduler_run_queue', 'Loopback' );

		// Fallback: spawn WP-Cron for hosts where loopback may be blocked.
		wp_remote_post(
			site_url( 'wp-cron.php' ),
			array(
				'timeout'   => 0.01,
				'blocking'  => false,
				// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core filter.
				'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
			)
		);

		self::update_metrics( $source );

		if ( class_exists( 'Agentic_WP\Error_Handler' ) ) {
			Error_Handler::debug_log(
				'Loopback spawned',
				array(
					'source' => $source,
				)
			);
		}

		return true;
	}

	/**
	 * Update health metrics with latest loopback spawn info.
	 *
	 * @param string $source Trigger source.
	 */
	private static function update_metrics( string $source ): void {
		$metrics = get_transient( self::METRICS_TRANSIENT );
		if ( ! is_array( $metrics ) ) {
			$metrics = array();
		}

		$metrics[ $source ] = array(
			'last_spawn' => time(),
		);

		set_transient( self::METRICS_TRANSIENT, $metrics, HOUR_IN_SECONDS );
	}

	/**
	 * Get current health metrics.
	 *
	 * @return array Health metrics with last_spawn times per source.
	 */
	public static function get_metrics(): array {
		$metrics = get_transient( self::METRICS_TRANSIENT );

		return is_array( $metrics ) ? $metrics : array();
	}

	/**
	 * Handle AJAX request to spawn loopback.
	 *
	 * Used by frontend beacon and can be used for manual triggers.
	 */
	public static function handle_spawn_loopback_ajax(): void {
		// Allow both logged-in and logged-out requests (frontend beacon).
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public endpoint for job processing trigger.
		$source = isset( $_POST['source'] ) ? sanitize_key( $_POST['source'] ) : 'ajax';

		$valid_sources = array( 'frontend', 'heartbeat', 'piggyback', 'manual', 'ajax' );
		if ( ! in_array( $source, $valid_sources, true ) ) {
			$source = 'ajax';
		}

		$spawned = self::spawn_as_loopback( $source );

		wp_send_json_success(
			array(
				'spawned' => $spawned,
				'source'  => $source,
			)
		);
	}

	/**
	 * Set the pending jobs flag.
	 *
	 * Called when a new job is scheduled.
	 */
	public static function set_pending_jobs_flag(): void {
		set_transient( self::PENDING_JOBS_TRANSIENT, true, HOUR_IN_SECONDS );
	}

	/**
	 * Clear the pending jobs flag.
	 *
	 * Called when no more pending jobs exist.
	 */
	public static function clear_pending_jobs_flag(): void {
		delete_transient( self::PENDING_JOBS_TRANSIENT );
	}

	/**
	 * Check if pending jobs flag is set.
	 *
	 * @return bool True if flag is set.
	 */
	public static function is_pending_jobs_flag_set(): bool {
		return (bool) get_transient( self::PENDING_JOBS_TRANSIENT );
	}

	/**
	 * Inject frontend beacon script to trigger loopback on page load.
	 *
	 * Only injects script if pending jobs flag is set to avoid overhead on every pageview.
	 * Uses sessionStorage to rate-limit beacon triggers per browser session.
	 */
	public static function inject_frontend_beacon(): void {
		// Only inject if there are pending jobs.
		if ( ! self::is_pending_jobs_flag_set() ) {
			return;
		}

		$ajax_url = admin_url( 'admin-ajax.php' );
		?>
		<script>
		(function() {
			if (!navigator.sendBeacon) return;
			var key = 'agenticwp_beacon';
			if (sessionStorage.getItem(key)) return;
			sessionStorage.setItem(key, '1');
			var formData = new FormData();
			formData.append('action', 'agenticwp_spawn_loopback');
			formData.append('source', 'frontend');
			navigator.sendBeacon(<?php echo wp_json_encode( $ajax_url ); ?>, formData);
		})();
		</script>
		<?php
	}
}
