<?php
/**
 * Primary class for the Jetpack Scan package.
 *
 * @package automattic/jetpack-scan-page
 */

namespace Automattic\Jetpack\Scan_Page;

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

use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
use function add_action;
use function add_filter;
use function apply_filters;
use function call_user_func;
use function current_user_can;
use function did_action;
use function do_action;
use function function_exists;
use function is_admin;
use function is_multisite;
use function remove_action;
use function remove_all_actions;
use function sanitize_text_field;
use function wp_unslash;

/**
 * Class Jetpack_Scan
 *
 * Registers the Scan admin page and its REST routes inside the main
 * Jetpack plugin. The page bundle is built by `@wordpress/build`
 * (mirroring Newsletter / Forms); this class wires the wp-admin menu
 * + the bridges that route our user-facing slug to wp-build's
 * auto-generated enqueue / render functions.
 */
class Jetpack_Scan {

	/**
	 * URL-facing menu slug.
	 *
	 * @var string
	 */
	const PAGE_SLUG = 'jetpack-scan';

	/**
	 * Internal slug emitted by `@wordpress/build` (`wpPlugin.pages[0]`
	 * plus the `-wp-admin` suffix the build template appends). Used to
	 * find the auto-generated render / enqueue functions.
	 *
	 * @var string
	 */
	const WP_BUILD_SLUG = 'jetpack-scan-wp-admin';

	/**
	 * Filter name that gates the wp-build–based Scan dashboard.
	 *
	 * When this filter returns true, the new wp-admin Scan page is
	 * registered and rendered. Default false during the modernization
	 * roll-out — the package registers no admin menu and changes
	 * nothing about the existing Jetpack UI when this filter is off.
	 *
	 * @var string
	 */
	const MODERNIZATION_FILTER = 'rsm_jetpack_ui_modernization_scan';

	/**
	 * Entry point. Idempotent: safe to call from multiple bootstraps.
	 */
	public static function initialize() {
		if ( did_action( 'jetpack_scan_page_initialized' ) ) {
			return;
		}

		if ( ! (bool) apply_filters( self::MODERNIZATION_FILTER, false ) ) {
			return;
		}

		self::load_wp_build();
		self::bridge_wp_build_enqueue();

		add_action( 'admin_menu', array( __CLASS__, 'add_wp_admin_submenu' ) );
		add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
		add_filter( 'jetpack_package_versions', array( Package_Version::class, 'send_package_version_to_tracker' ) );

		/**
		 * Fires once the Jetpack Scan package has wired its hooks.
		 *
		 * @since 0.1.0
		 */
		do_action( 'jetpack_scan_page_initialized' );
	}

	/**
	 * Load wp-build generated registration files. Mirrors Newsletter / Forms.
	 */
	public static function load_wp_build() {
		// The polyfills force-replace core script handles (notably
		// `wp-private-apis`, a stateful singleton shared by every @wordpress
		// package on the page) during `wp_default_scripts`. Scope registration
		// to the Scan admin page so it never runs on other admin pages such as
		// the block editor. `$_GET['page']` is reliable this early — it's the
		// raw query param, available well before `current_screen` exists.
		if ( self::is_scan_admin_request() ) {
			WP_Build_Polyfills::register(
				'jetpack-scan',
				array_merge( WP_Build_Polyfills::SCRIPT_HANDLES, WP_Build_Polyfills::MODULE_IDS )
			);
		}

		$wp_build_index = dirname( __DIR__ ) . '/build/build.php';
		if ( file_exists( $wp_build_index ) ) {
			require_once $wp_build_index;
		}

		// `page.php` ships an `admin_init` interceptor that takes over our
		// slug with a standalone (non-wp-admin) render. We want the
		// wp-admin integrated experience, so unregister it as soon as it's
		// loaded.
		remove_action(
			'admin_init',
			'jetpack_scan_jetpack_scan_intercept_render'
		);
	}

	/**
	 * Whether the current request targets the Scan admin page.
	 *
	 * Used to scope the wp-build polyfill registration (which force-replaces
	 * core script handles) to this one page, so it never affects other admin
	 * pages. Reads the menu page slug directly so it is cheap and safe to call
	 * at plugin-load time, before `current_screen` exists.
	 *
	 * @return bool True when serving the Scan page in wp-admin.
	 */
	public static function is_scan_admin_request() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! is_admin() || ! isset( $_GET['page'] ) ) {
			return false;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		return self::PAGE_SLUG === sanitize_text_field( wp_unslash( $_GET['page'] ) );
	}

	/**
	 * Bridge wp-build's auto-generated enqueue function — which checks for
	 * `?page=jetpack-scan-wp-admin` — to our user-facing slug
	 * `?page=jetpack-scan`. Hooked at priority 9 so the wp-build copy
	 * (registered at priority 10) sees the original `$_GET['page']` and
	 * skips its own enqueue.
	 */
	public static function bridge_wp_build_enqueue() {
		add_action(
			'admin_enqueue_scripts',
			static function ( $hook_suffix ) {
				// phpcs:ignore WordPress.Security.NonceVerification.Recommended
				if ( ! isset( $_GET['page'] ) || self::PAGE_SLUG !== $_GET['page'] ) {
					return;
				}

				$enqueue_fn = 'jetpack_scan_jetpack_scan_wp_admin_enqueue_scripts';
				if ( ! function_exists( $enqueue_fn ) ) {
					return;
				}

				// phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				$original     = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : null;
				$_GET['page'] = self::WP_BUILD_SLUG;
				// @phan-suppress-next-line PhanUndeclaredFunctionInCallable -- Function is generated by @wordpress/build into build/pages/jetpack-scan/page-wp-admin.php, which is outside Phan's analysis scope. The function_exists() guard above protects the call at runtime.
				call_user_func( $enqueue_fn, $hook_suffix );
				if ( null === $original ) {
					unset( $_GET['page'] );
				} else {
					$_GET['page'] = $original;
				}
				// phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			},
			9
		);
	}

	/**
	 * Register the Scan submenu under Jetpack.
	 *
	 * @return string|null The resulting page's hook suffix, if registered.
	 */
	public static function add_wp_admin_submenu() {
		if ( ! self::is_available() ) {
			return null;
		}

		$render_fn = 'jetpack_scan_jetpack_scan_wp_admin_render_page';
		$render    = function_exists( $render_fn )
			? $render_fn
			: array( __CLASS__, 'render_page_fallback' );

		$page_suffix = Admin_Menu::add_menu(
			/** "Scan" is a product name, do not translate. */
			'Scan',
			'Scan',
			'manage_options',
			self::PAGE_SLUG,
			$render,
			6
		);

		if ( $page_suffix ) {
			add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
		}

		return $page_suffix;
	}

	/**
	 * Whether the Scan page should be shown to the current user.
	 *
	 * @return bool
	 */
	public static function is_available() {
		if ( is_multisite() ) {
			return false;
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return false;
		}

		return ( new Connection_Manager() )->is_user_connected();
	}

	/**
	 * Fires when the admin page is loaded.
	 *
	 * Silences the standard wp-admin notice channels so JITMs and
	 * plugin-update messages don't reflow the focused Scan layout
	 * mid-scan or while a fix modal is open.
	 */
	public static function admin_init() {
		remove_all_actions( 'admin_notices' );
		remove_all_actions( 'all_admin_notices' );
	}

	/**
	 * Fallback render — used only if the wp-build registration file
	 * isn't loaded (e.g. the package wasn't built yet). Renders a bare
	 * mount node so the page doesn't 500 in dev.
	 */
	public static function render_page_fallback() {
		?>
			<div id="jetpack-scan-page-root"></div>
		<?php
	}

	/**
	 * Register the REST routes backing the Scan UI.
	 */
	public static function register_rest_routes() {
		REST_Controller::register_rest_routes();
	}
}
