<?php
/**
 * File to add admin menus.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * Class to register admin menu.
 */
class SimpleStats_Admin_Menu {

	/**
	 * Hold the instance.
	 *
	 * @var self
	 */
	private static $instance;

	/**
	 * Get instance of class.
	 *
	 * @return self
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor function of the class.
	 */
	private function __construct() {
		add_action( 'admin_menu', array( $this, 'register_menu_page' ) );
		add_filter( 'admin_body_class', array( $this, 'admin_classes' ) );
		add_action( 'admin_head', array( $this, 'disable_admin_notices' ) );
		add_action( 'current_screen', array( $this, 'set_admin_page_title' ) );
		// Must fire before admin-header.php streams output, otherwise
		// wp_safe_redirect() warns "headers already sent" and the redirect fails.
		add_action( 'current_screen', array( $this, 'maybe_redirect_connected_user' ) );
		// Hide the auth submenu after the permission check has run but before
		// menu-header.php renders. Removing it in `admin_menu` (too early)
		// breaks user_can_access_admin_page() because get_admin_page_parent()
		// can no longer resolve the parent of the auth page.
		add_action( 'current_screen', array( $this, 'hide_auth_submenu' ) );
	}

	/**
	 * Add menu and submenu.
	 */
	public function register_menu_page() {
		add_menu_page(
			__( 'SimpleStats', 'simplestats-friendly-website-analytics' ),
			__( 'SimpleStats', 'simplestats-friendly-website-analytics' ),
			'manage_options',
			'simplestats',
			array( $this, 'dashboard_page' ),
			'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iLTIgLTIgMjAgMjAiIGZpbGw9Im5vbmUiPgogIDxwYXRoIGQ9Ik03LjU0MzU3IDAuMDE5NTAyOEM5LjA0OTczIC0wLjA5OTczMjIgMTAuNzIwNCAwLjMzNjI4NiAxMi4wMjU3IDEuMDkzOTJDMTMuODUzNyAyLjE1MjkgMTUuMTkwNCAzLjkwODc1IDE1LjczOTMgNS45NzIyMkMxNi4yNyA3Ljk5NzczIDE1Ljk4MzUgMTAuMTU0NSAxNC45NDM1IDExLjk2NDlDMTMuODc5NSAxMy44MTgxIDEyLjEzNTQgMTUuMTY3NCAxMC4wOTM2IDE1LjcxN0M3LjkzMzgzIDE2LjMwMiA1Ljc2Nzk0IDE1Ljk2NjYgMy44NDI1NyAxNC44MzU5QzUuMTY4OTcgMTQuNDIwNyA2LjQ1NDI3IDEzLjg4MDUgNy42ODE3MSAxMy4yMjIzQzguNzM2OSAxMi42Njk1IDkuNzY5NTIgMTIuMDQ3OSAxMC43NzAzIDExLjM5OTRDMTEuMDM2MiAxMS4xOTU3IDExLjc1MDEgMTAuOTEzMSAxMS44MjM5IDEwLjU2NDVDMTEuOTE2IDEwLjA5MzUgMTEuODMyNSA5LjU4MDE4IDExLjg1MTcgOS4wOTkyN0MxMS44Njg0IDguNTA5MjYgMTEuNTI0NCA4LjQ2NDc5IDExLjA3MTkgOC4zMjYzNEM5Ljg1MzI2IDcuOTUzNjkgOC42MTM2NiA3LjY0MzE2IDcuMzkzNyA3LjI4MjExQzguMzI4NTkgNi44NTAwMSA5LjM1NjAyIDYuMzE1NiAxMC4yNzM4IDUuODQ0MzJDMTAuNzY5NCA1LjU5MDc0IDExLjI4ODMgNS4zMjAyIDExLjc2NjIgNS4wMzU0N0MxMS45NzIgNS4yMjk3MSAxMi42MzAyIDUuOTMzNTUgMTIuODE1MiA2LjAzMDA0QzEyLjg5NzQgNi4wMzM3MSAxMi44NjQ1IDYuMDQyMzUgMTIuOTM5MiA1Ljk4OTA3QzEyLjk3MTIgNS45MjkxOSAxMi45ODUyIDUuODUyMzQgMTIuOTg5MyA1Ljc4NDM3QzEzLjA1MDQgNC43MjIxNSAxMy4xMjExIDMuNjU5MDUgMTMuMTc0MSAyLjU5NzAxQzEzLjE3NzcgMi41MjE0OSAxMy4xNDgyIDIuNDU4MTkgMTMuMDkyNiAyLjQxMDYyQzEzLjAwOCAyLjM5MTIgMTIuNjE2NiAyLjQwMjM3IDEyLjU0MDggMi40MTgzN0MxMi4xNDI0IDIuNTAyMzUgOS44MTM5OSAyLjUyNTE4IDkuNjIzOTQgMi42MzY2NEM5LjU1Mjg0IDIuNzQxMjMgOS42MDA5MiAyLjgzMDEzIDkuNjgwNTkgMi45MTU2N0M5LjkxOTM5IDMuMTcxMzYgMTAuMTkxOCAzLjQyNjcyIDEwLjQ0MDYgMy42NzAzNUM5LjI4NjA1IDQuMTk1NTUgOC4xMzUxNiA0LjcyODk4IDYuOTg3OTkgNS4yNzA1OUM2LjM5MjM0IDUuNTUwODUgNS43OTEyMSA1LjgzMTA1IDUuMTk5ODcgNi4xMTk3N0M1LjA2MDk5IDYuMTk2MzkgNC44OTgzMiA2LjI2NzY4IDQuODYzNTYgNi40Mzk1NkM0Ljc5MjUzIDYuNzkwNjggNC44NDcxNiA3LjE3ODE5IDQuODI3NjMgNy41MzY1MUM0LjgyMDYzIDcuNjg5MTcgNC44MDExMSA4LjI3NDc4IDQuOTE0OCA4LjM3NTg4QzUuMTk0NTkgOC42MjQ2OSA2LjM5MzIyIDguOTE4NDggNi43MTUwMSA5LjAwNTY2QzcuNTA4NTkgOS4yMjA3MSA4LjI3MTA0IDkuNDgyOTMgOS4wMzg2NyA5LjY2MjUyQzYuOTYxODcgMTAuNzg0NiAzLjg0OTM4IDEyLjI0NDUgMS41MDUzMiAxMi42NTI4QzAuNjY4NDU2IDExLjU5MDUgMC4xMTUzMSA5Ljk1MDkgMC4wMjMyNDI5IDguNjAyNTdDLTAuMTM4NDc4IDYuNDg1NTIgMC41NDg3MTcgNC4zOTE5MyAxLjkyODgyIDIuNzk3MDFDMy4zODk1IDEuMDk5ODkgNS4zNDIgMC4xNzEyNDYgNy41NDM1NyAwLjAxOTUwMjhaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4='
		);

		add_submenu_page(
			'simplestats',
			__( 'WP Connector', 'simplestats-friendly-website-analytics' ),
			__( 'Dashboard', 'simplestats-friendly-website-analytics' ),
			'manage_options',
			'simplestats#/dashboard',
			array( $this, 'dashboard_page' )
		);

		add_submenu_page(
			'simplestats',
			__( 'Settings', 'simplestats-friendly-website-analytics' ),
			__( 'Settings', 'simplestats-friendly-website-analytics' ),
			'manage_options',
			'simplestats#/settings',
			array( $this, 'dashboard_page' )
		);

		add_submenu_page(
			'simplestats',
			__( 'WP Connector', 'simplestats-friendly-website-analytics' ),
			__( 'About Us', 'simplestats-friendly-website-analytics' ),
			'manage_options',
			'simplestats#/about-us',
			array( $this, 'dashboard_page' )
		);

		// Register the auth page as a real child of `simplestats` (not parent=''),
		// so WordPress naturally highlights the SimpleStats top-level menu while
		// the user is on the auth screen. Hidden from the sidebar via
		// remove_submenu_page() below.
		add_submenu_page(
			'simplestats',
			__( 'Connect to SimpleStats', 'simplestats-friendly-website-analytics' ),
			__( 'Connect to SimpleStats', 'simplestats-friendly-website-analytics' ),
			'manage_options',
			'simplestats_authentication',
			array( $this, 'authentication_page' )
		);

		// Remove own submenu of WP Connector.
		remove_submenu_page( 'simplestats', 'simplestats' );
	}

	/**
	 * Strip the auth page from the sidebar after WP's permission check has
	 * already validated access — see the comment on the action hookup above.
	 */
	public function hide_auth_submenu() {
		remove_submenu_page( 'simplestats', 'simplestats_authentication' );
	}

	/**
	 * Callback for dashboard page.
	 */
	public function dashboard_page() {
		echo '<div id="simplestats-vue-app"></div>';
	}

	/**
	 * Bounce already-connected users away from the auth page early — before
	 * admin-header.php streams output. URL hash fragments aren't sent to the
	 * server, so we always land them on the settings screen regardless of
	 * whether they deep-linked to /sign-up, /sign-in-success, etc.
	 *
	 * @param WP_Screen $screen Current screen.
	 */
	public function maybe_redirect_connected_user( $screen ) {
		if ( ! isset( $screen->id ) ) {
			return;
		}
		if ( 'simplestats_page_simplestats_authentication' !== $screen->id ) {
			return;
		}
		if ( ! simplestats_is_connected() ) {
			return;
		}

		wp_safe_redirect( admin_url( 'admin.php?page=simplestats#/settings' ) );
		exit;
	}

	/**
	 * Callback for the hidden authentication admin page. Renders the SPA mount
	 * and lazily backfills the installation_id for upgrade scenarios. The
	 * connected-user redirect happens earlier on `current_screen`.
	 */
	public function authentication_page() {
		simplestats_get_installation_id();

		echo '<div id="simplestats-authentication-app"></div>';
	}

	/**
	 * Seed $title for our hidden authentication page so admin-header.php's
	 * strip_tags( $title ) doesn't get a null (the page has no discoverable
	 * parent and get_admin_page_title() can't resolve it).
	 *
	 * @param WP_Screen $screen Current screen.
	 */
	public function set_admin_page_title( $screen ) {
		if ( isset( $screen->id ) && 'simplestats_page_simplestats_authentication' === $screen->id ) {
			// Seeding $title is the only way to keep admin-header.php's
			// strip_tags( $title ) off a null for a page whose submenu entry we
			// remove; there is no filter that runs early enough. Scoped to this
			// one screen, and it is our own page's title.
			// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
			$GLOBALS['title'] = __( 'Connect to SimpleStats', 'simplestats-friendly-website-analytics' );
		}
	}

	/**
	 * Add own class to admin body.
	 */
	public function admin_classes( $classes ) {
		$screen = get_current_screen();

		// get_current_screen() can return null this early on some screens;
		// dereferencing it unguarded is a PHP 8 warning.
		if ( ! isset( $screen->id ) ) {
			return $classes;
		}

		if ( 'toplevel_page_simplestats' === $screen->id ) {
			$classes .= ' simplestats-pages-body';
		}

		if ( 'simplestats_page_simplestats_authentication' === $screen->id ) {
			$classes .= ' simplestats-pages-body simplestats-authentication-body';
		}

		return $classes;
	}

	/**
	 * Quiet unrelated admin notices on our two full-screen app pages.
	 *
	 * Deliberately NOT remove_all_actions(): that also swallowed core's own
	 * notices, so an administrator sitting on this screen would never be told
	 * that WordPress has an update waiting, that the site is in maintenance or
	 * recovery mode, or that a plugin had to be deactivated. Those are exactly
	 * the messages a plugin has no business hiding.
	 *
	 * Instead every registered callback is walked and only the ones we can
	 * positively identify as core are kept. Unidentifiable callbacks (closures,
	 * invokable objects) are treated as third-party, since that is what they
	 * almost always are.
	 *
	 * @return void
	 */
	public function disable_admin_notices() {
		global $current_screen;

		$is_simplestats = isset( $current_screen->id ) && (
			'toplevel_page_simplestats' === $current_screen->id ||
			'simplestats_page_simplestats_authentication' === $current_screen->id
		);

		if ( ! $is_simplestats ) {
			return;
		}

		$allowed = $this->protected_notice_callbacks();

		foreach ( array( 'admin_notices', 'all_admin_notices' ) as $hook ) {
			$this->remove_unprotected_notices( $hook, $allowed );
		}
	}

	/**
	 * Notice callbacks that must survive on our screens.
	 *
	 * These are core's own registrations (wp-admin/includes/admin-filters.php,
	 * ms-admin-filters.php and WP_Privacy_Policy_Content). Extendable so a site
	 * can protect a callback of its own.
	 *
	 * @return string[] Function names, or "Class::method" for method callbacks.
	 */
	private function protected_notice_callbacks() {
		return (array) apply_filters(
			'simplestats_protected_admin_notices',
			array(
				'update_nag',
				'maintenance_nag',
				'wp_recovery_mode_nag',
				'deactivated_plugins_notice',
				'paused_plugins_notice',
				'paused_themes_notice',
				'default_password_nag',
				'new_user_email_admin_notice',
				'site_admin_notice',
				'WP_Privacy_Policy_Content::notice',
				'WP_Privacy_Policy_Content::policy_text_changed_notice',
			)
		);
	}

	/**
	 * Remove every callback on $hook that is not in the allow-list.
	 *
	 * @param string   $hook    Hook name.
	 * @param string[] $allowed Callback identifiers to keep.
	 * @return void
	 */
	private function remove_unprotected_notices( $hook, $allowed ) {
		global $wp_filter;

		if ( empty( $wp_filter[ $hook ] ) || ! is_object( $wp_filter[ $hook ] ) ) {
			return;
		}

		// Snapshot before iterating: remove_action() mutates the live callbacks
		// array, and walking it while it is being modified skips entries.
		$registered = $wp_filter[ $hook ]->callbacks;

		foreach ( $registered as $priority => $callbacks ) {
			foreach ( $callbacks as $callback ) {
				if ( ! isset( $callback['function'] ) ) {
					continue;
				}

				if ( in_array( $this->callback_identifier( $callback['function'] ), $allowed, true ) ) {
					continue;
				}

				remove_action( $hook, $callback['function'], $priority );
			}
		}
	}

	/**
	 * Reduce a callable to a comparable identifier.
	 *
	 * @param callable $callback Registered callback.
	 * @return string Empty string when the callable cannot be identified.
	 */
	private function callback_identifier( $callback ) {
		if ( is_string( $callback ) ) {
			return $callback;
		}

		if ( is_array( $callback ) && isset( $callback[0], $callback[1] ) ) {
			$class = is_object( $callback[0] ) ? get_class( $callback[0] ) : (string) $callback[0];

			return $class . '::' . $callback[1];
		}

		return '';
	}
}
