<?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_multisite;
use function remove_action;
use function remove_all_actions;
use function sanitize_text_field;
use function wp_print_inline_script_tag;
use function wp_scripts;
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::fix_boot_import_map_ordering();
		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() {
		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'
		);
	}

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

	/**
	 * Fix import map ordering for the wp-build boot script.
	 *
	 * In wp-admin, `_wp_footer_scripts` (classic scripts) and
	 * `print_import_map` both hook into `admin_print_footer_scripts` at
	 * priority 10, but `_wp_footer_scripts` is registered first. This
	 * causes the inline `import("@wordpress/boot")` to execute before
	 * the import map exists.
	 *
	 * This fix moves the import() call from the classic inline script to
	 * a `<script type="module">` printed at priority 20 (after the import
	 * map).
	 *
	 * @todo Remove once @wordpress/build ships with the loader.js fix
	 *       upstream (WordPress/gutenberg#76870) and Jetpack updates the
	 *       dependency.
	 */
	public static function fix_boot_import_map_ordering() {
		$handle = self::WP_BUILD_SLUG . '-prerequisites';

		add_action(
			'admin_enqueue_scripts',
			static function () use ( $handle ) {
				// phpcs:ignore WordPress.Security.NonceVerification.Recommended
				if ( ! isset( $_GET['page'] ) || self::PAGE_SLUG !== $_GET['page'] ) {
					return;
				}

				$data = wp_scripts()->get_data( $handle, 'after' );
				if ( empty( $data ) ) {
					return;
				}

				$boot_script = null;
				$remaining   = array();
				foreach ( $data as $line ) {
					if ( strpos( $line, '@wordpress/boot' ) !== false ) {
						$boot_script = $line;
					} else {
						$remaining[] = $line;
					}
				}

				if ( null === $boot_script ) {
					return;
				}

				wp_scripts()->add_data( $handle, 'after', $remaining );

				add_action(
					'admin_print_footer_scripts',
					static function () use ( $boot_script ) {
						wp_print_inline_script_tag( $boot_script, array( 'type' => 'module' ) );
					},
					20
				);
			},
			PHP_INT_MAX
		);
	}

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