<?php

namespace Waypoint\Agent\Compat;

use Waypoint\Agent\Base\Thing;

class Rank_Math_Pro_Update_Guard extends Thing {

	const TRANSIENT_RUNNING  = 'waypoint_rm_update_running';
	const OPTION_SCHEMA_OK   = 'waypoint_rm_schema_ok';

	public function attach_hooks(): void {
		add_action( 'admin_init', [ $this, 'maybe_suppress_update_chain' ], 0 );
		add_action( 'waypoint_monitor_daily_heartbeat', [ $this, 'maybe_check_schema' ] );
	}

	// -------------------------------------------------------------------------
	// Part 1 + 2: Ajax suppression and loopback guard
	// -------------------------------------------------------------------------

	/**
	 * Runs on admin_init at priority 0 — before Rank Math's do_updates (priority 10).
	 *
	 * On ajax requests: remove the do_updates callback so it never fires.
	 * The update chain calls check_ajax_referer() internally, which wp_die(-1, 403)s
	 * on any admin-ajax request, killing every unrelated ajax call site-wide.
	 * It is also self-perpetuating: dispatch() fires a loopback POST to admin-ajax.php,
	 * which re-enters do_updates, hits check_ajax_referer(), and dies before
	 * perform_updates() can write rank_math_pro_version. So stuck sites never
	 * self-heal from a normal admin page load either.
	 *
	 * On normal admin loads where version is behind: add a pre_http_request filter
	 * that short-circuits any loopback POST to admin-ajax.php. With the loopback
	 * returning a fake 200, dispatch() completes, perform_updates() runs to the end,
	 * and rank_math_pro_version is written. The guard is removed after admin_init.
	 */
	public function maybe_suppress_update_chain(): void {
		if ( ! class_exists( '\RankMathPro\Updates' ) ) {
			return;
		}

		global $wp_filter;

		foreach ( [ 'admin_init', 'init' ] as $hook ) {
			if ( empty( $wp_filter[ $hook ] ) ) {
				continue;
			}
			foreach ( $wp_filter[ $hook ]->callbacks as $priority => $callbacks ) {
				foreach ( $callbacks as $id => $cb ) {
					$fn = $cb['function'];
					if ( is_array( $fn )
						&& is_object( $fn[0] )
						&& $fn[0] instanceof \RankMathPro\Updates
						&& 'do_updates' === $fn[1]
					) {
						if ( wp_doing_ajax() ) {
							remove_action( $hook, $fn, $priority );
						}
					}
				}
			}
		}

		if ( wp_doing_ajax() ) {
			return;
		}

		$this->maybe_install_loopback_guard();
	}

	private function maybe_install_loopback_guard(): void {
		if ( ! defined( 'RANK_MATH_PRO_VERSION' ) ) {
			return;
		}

		$stored = get_option( 'rank_math_pro_version', '1.0.0' );
		if ( version_compare( $stored, RANK_MATH_PRO_VERSION, '>=' ) ) {
			return;
		}

		// One attempt per 5 minutes across all requests so a burst cannot stack.
		if ( get_transient( self::TRANSIENT_RUNNING ) ) {
			return;
		}
		set_transient( self::TRANSIENT_RUNNING, 1, 5 * MINUTE_IN_SECONDS );

		// Short-circuit loopback POSTs to admin-ajax.php so dispatch() cannot
		// re-enter do_updates() and die before perform_updates() writes the option.
		add_filter( 'pre_http_request', [ $this, 'short_circuit_ajax_loopback' ], PHP_INT_MAX, 3 );

		// Remove the guard after admin_init finishes — update runs at priority 10.
		add_action( 'admin_init', [ $this, 'remove_loopback_guard' ], PHP_INT_MAX );
	}

	public function short_circuit_ajax_loopback( mixed $preempt, array $parsed_args, string $url ): mixed {
		if ( str_contains( $url, 'admin-ajax.php' ) ) {
			return [
				'headers'  => [],
				'body'     => '',
				'response' => [ 'code' => 200, 'message' => 'OK' ],
				'cookies'  => [],
				'filename' => '',
			];
		}
		return $preempt;
	}

	public function remove_loopback_guard(): void {
		remove_filter( 'pre_http_request', [ $this, 'short_circuit_ajax_loopback' ], PHP_INT_MAX );
	}

	// -------------------------------------------------------------------------
	// Part 3: Daily schema check
	// -------------------------------------------------------------------------

	/**
	 * Verifies Rank Math Pro's Link Genius tables exist and calls Rank Math's
	 * own table creator if any are missing.
	 *
	 * Guards with a version-keyed option (waypoint_rm_schema_ok = "x.y.z") so the
	 * check re-runs exactly once after each Rank Math Pro upgrade — a plain boolean
	 * would suppress it permanently and miss schema changes in future releases.
	 *
	 * Set the constant WAYPOINT_FORCE_RM_SCHEMA_CHECK to true to force a run
	 * regardless of the stored version, for debugging.
	 */
	public function maybe_check_schema(): void {
		if ( ! class_exists( '\RankMathPro\Link_Genius\Data\Table_Extension' ) ) {
			return;
		}
		if ( ! defined( 'RANK_MATH_PRO_VERSION' ) ) {
			return;
		}

		$force = defined( 'WAYPOINT_FORCE_RM_SCHEMA_CHECK' ) && WAYPOINT_FORCE_RM_SCHEMA_CHECK;

		if ( ! $force ) {
			if ( get_option( self::OPTION_SCHEMA_OK ) === RANK_MATH_PRO_VERSION ) {
				return;
			}
		}

		try {
			if ( method_exists( '\RankMathPro\Link_Genius\Data\Table_Extension', 'ensure_all_pro_tables_exist' ) ) {
				\RankMathPro\Link_Genius\Data\Table_Extension::ensure_all_pro_tables_exist();
			} else {
				\RankMathPro\Link_Genius\Data\Table_Extension::initialize_schema();
			}
			update_option( self::OPTION_SCHEMA_OK, RANK_MATH_PRO_VERSION, false );
		} catch ( \Throwable $e ) {
			error_log( 'Waypoint Agent: Rank Math Pro schema check failed — ' . $e->getMessage() );
		}
	}
}
