<?php

namespace AdminMenuOrganizer;

use WP_PowerSuite\Core\AbstractModule;
class Module extends AbstractModule {

	private const MODULE_ID = 'admin-menu-organizer';
	private const MODULE_VERSION = '1.4.0';
	public const MENU_CONFIG_OPTION = 'wp_powersuite_admin_menu_organizer_config';
	private const MAX_MENU_CONFIG_BYTES = 1048576;
	private const MAX_MENU_CONFIG_ITEMS = 500;
	private const MENU_CONFIG_MIGRATED_FLAG = 'wp_powersuite_admin_menu_config_migrated';

	private ?array $settings_cache = null;

	/** @var array{user_id: int, user_roles: string[]}|null */
	private ?array $hide_rules_user_context = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Admin Menu Organizer', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Reorder, rename, or hide sidebar menu items so the dashboard fits how you work.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'ajax' ];
	}

	public function getDependencies(): array {
		return [];
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		add_action( $this->adminMenuHook(), [ $this, 'addAdminMenu' ], 20 );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScripts' ] );

		// Must register here — this module boots on admin_menu (priority 0), after admin_init has already run.
		$this->registerMenuApplicationHooks();

		add_action( 'wp_ajax_wpps_save_menu_config', [ $this, 'handleSaveMenuConfig' ] );
		add_action( 'wp_ajax_wpps_get_menu_config', [ $this, 'handleGetMenuConfig' ] );
		add_action( 'wp_ajax_wpps_restore_menu_config', [ $this, 'handleRestoreMenuConfig' ] );
		add_action( 'wp_ajax_wpps_search_users', [ $this, 'handleSearchUsers' ] );
	}


	public function addAdminMenu(): void {
		$this->registerPowerSuiteSubmenu(
			__( 'Admin Menu Organizer', 'wp-powersuite' ),
			__( 'Menu Organizer', 'wp-powersuite' ),
			'powersuite-admin-menu-organizer',
			[ $this, 'renderAdminPage' ]
		);
	}

	public function renderAdminPage(): void {
		if ( ! $this->canManagePlugin() ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-powersuite' ) );
		}

		$saved_menu_config = get_option( self::MENU_CONFIG_OPTION, null );
		if ( ! is_array( $saved_menu_config ) ) {
			$settings          = $this->getSettings();
			$saved_menu_config = $settings['menu_config'] ?? null;
		}

		$menu_config = $this->getEditorMenuConfig( is_array( $saved_menu_config ) ? $saved_menu_config : null );
		$users_map   = $this->buildUsersMapFromConfig( $menu_config );

		include __DIR__ . '/admin.php';
	}

	public function enqueueScripts( string $hook ): void {
		if ( $hook !== 'wp-powersuite_page_powersuite-admin-menu-organizer' ) {
			return;
		}

		wp_enqueue_script( 'jquery-ui-sortable' );
		wp_enqueue_style( 'wp-powersuite-admin-menu-organizer', $this->getAssetUrl( 'admin-menu-organizer.css' ), [], self::MODULE_VERSION );
		wp_enqueue_script( 'wp-powersuite-admin-menu-organizer', $this->getAssetUrl( 'admin-menu-organizer.js' ), [ 'jquery', 'jquery-ui-sortable' ], self::MODULE_VERSION, true );

		wp_localize_script( 'wp-powersuite-admin-menu-organizer', 'wppsMenuOrganizer', [
			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			'nonce'   => wp_create_nonce( 'wpps_menu_organizer' ),
			'strings' => [
				'saving'        => __( 'Saving...', 'wp-powersuite' ),
				'saved'         => __( 'Menu configuration saved successfully!', 'wp-powersuite' ),
				'error'         => __( 'An error occurred. Please try again.', 'wp-powersuite' ),
				'confirmRestore' => __( 'Are you sure you want to restore the default menu? This will remove all customizations.', 'wp-powersuite' ),
				'restoring'     => __( 'Restoring...', 'wp-powersuite' ),
				'restored'      => __( 'Menu restored to defaults successfully!', 'wp-powersuite' ),
				'refreshing'    => __( 'Refreshing...', 'wp-powersuite' ),
				'allSelected'   => __( 'All matching users already selected.', 'wp-powersuite' ),
				'hideModeToggle' => __( 'Collapse under "Show All"', 'wp-powersuite' ),
				'hideModeRemove' => __( 'Remove from menu', 'wp-powersuite' ),
				'confirmDelete'  => __( 'Are you sure you want to delete this menu item?', 'wp-powersuite' ),
				'confirmDeleteSub' => __( 'Are you sure you want to delete this submenu item?', 'wp-powersuite' ),
				'requiredFields'   => __( 'Title and URL/Menu Slug are required.', 'wp-powersuite' ),
				'selectRole'       => __( 'Please select at least one role when "Specific Role(s)" is selected.', 'wp-powersuite' ),
				'duplicateSlug'    => __( 'A menu item with this slug already exists. Please use a unique slug.', 'wp-powersuite' ),
			],
		] );
	}

	/**
	 * Register hooks that reshape the live admin menu (only when saved config exists).
	 */
	private function registerMenuApplicationHooks(): void {
		if ( ! is_admin() ) {
			return;
		}

		$settings    = $this->getSettings();
		$menu_config = $settings['menu_config'] ?? null;
		if ( empty( $menu_config ) || ! is_array( $menu_config ) ) {
			return;
		}

		add_action( 'admin_menu', [ $this, 'registerCustomMenuPages' ], 98 );
		add_action( 'admin_menu', [ $this, 'applyMenuCustomizations' ], 999999 );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAppliedMenuAssets' ] );
		add_action( 'admin_head', [ $this, 'printAdminMenuOrganizerCss' ] );
		add_action( 'current_screen', [ $this, 'maybeRestrictScreenAccess' ], 1 );
	}

	/**
	 * Enqueue assets for applied menu customizations (open-in-new-tab, separator styling).
	 */
	public function enqueueAppliedMenuAssets( string $hook_suffix ): void {
		unset( $hook_suffix );

		$settings    = $this->getSettings();
		$menu_config = $settings['menu_config'] ?? null;
		if ( empty( $menu_config ) || ! is_array( $menu_config ) ) {
			return;
		}

		$asset_state = $this->collectAppliedMenuAssetState( $menu_config );

		if ( $asset_state['has_separators'] || $asset_state['has_custom_items'] || $asset_state['has_soft_hidden'] ) {
			wp_enqueue_style(
				'wp-powersuite-admin-menu-organizer-applied',
				$this->getAssetUrl( 'admin-menu-applied.css' ),
				[],
				self::MODULE_VERSION
			);
		}

		if ( $asset_state['has_soft_hidden'] ) {
			wp_enqueue_script(
				'wp-powersuite-admin-menu-organizer-toggle-hidden',
				$this->getAssetUrl( 'toggle-hidden-menu.js' ),
				[ 'jquery' ],
				self::MODULE_VERSION,
				true
			);
			wp_localize_script(
				'wp-powersuite-admin-menu-organizer-toggle-hidden',
				'wppsMenuOrganizerToggle',
				[
					'showAll'  => __( 'Show All', 'wp-powersuite' ),
					'showLess' => __( 'Show Less', 'wp-powersuite' ),
				]
			);
		}

		$needs_menu_links_script = ! empty( $asset_state['custom_links'] ) || ! empty( $asset_state['open_new_tab_slugs'] );
		if ( ! $needs_menu_links_script ) {
			return;
		}

		wp_enqueue_script(
			'wp-powersuite-admin-menu-organizer-applied',
			$this->getAssetUrl( 'admin-menu-applied.js' ),
			[ 'jquery' ],
			self::MODULE_VERSION,
			true
		);

		wp_localize_script(
			'wp-powersuite-admin-menu-organizer-applied',
			'wppsMenuOrganizerApplied',
			[
				'customLinks'     => $asset_state['custom_links'],
				'openNewTabSlugs' => $asset_state['open_new_tab_slugs'],
				'adminPageBase'   => admin_url( 'admin.php?page=' ),
			]
		);
	}

	/**
	 * Single tree walk for applied-menu CSS/JS flags (avoids multiple config scans).
	 *
	 * @param array<int, array<string, mixed>> $menu_config
	 * @return array{
	 *   has_separators: bool,
	 *   has_custom_items: bool,
	 *   has_soft_hidden: bool,
	 *   custom_links: array<string, array{url: string, openNewTab: bool}>,
	 *   open_new_tab_slugs: list<string>
	 * }
	 */
	private function collectAppliedMenuAssetState( array $menu_config ): array {
		$state = [
			'has_separators'     => false,
			'has_custom_items'   => false,
			'has_soft_hidden'    => false,
			'custom_links'       => [],
			'open_new_tab_slugs' => [],
		];

		$this->walkMenuConfigForAppliedAssetState( $menu_config, $state );

		$state['open_new_tab_slugs'] = array_values( array_unique( $state['open_new_tab_slugs'] ) );

		return $state;
	}

	/**
	 * @param array<int, array<string, mixed>> $items
	 * @param array<string, mixed>              $state
	 */
	private function walkMenuConfigForAppliedAssetState( array $items, array &$state ): void {
		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}

			if ( ! empty( $item['is_separator'] ) && empty( $item['hidden'] ) ) {
				$state['has_separators'] = true;
			}
			if ( ! empty( $item['is_custom'] ) && empty( $item['hidden'] ) ) {
				$state['has_custom_items'] = true;
			}
			if ( $this->isHiddenForCurrentUser( $item ) && $this->usesSoftHideBehavior( $item ) ) {
				$state['has_soft_hidden'] = true;
			}

			if ( ! empty( $item['is_custom'] ) ) {
				$slug = $this->getItemSlug( $item );
				$url  = $this->getCustomMenuTargetUrl( $item );
				if ( $slug !== '' && $url !== '' ) {
					$state['custom_links'][ $slug ] = [
						'url'        => $url,
						'openNewTab' => ! empty( $item['open_new_tab'] ),
					];
				}
			}

			if ( ! empty( $item['open_new_tab'] ) ) {
				$slug = $this->getItemSlug( $item );
				if ( $slug !== '' && ! isset( $state['custom_links'][ $slug ] ) ) {
					$state['open_new_tab_slugs'][] = $slug;
				}
			}

			if ( ! empty( $item['submenu'] ) && is_array( $item['submenu'] ) ) {
				$this->walkMenuConfigForAppliedAssetState( $item['submenu'], $state );
			}
		}
	}

	public function applyMenuCustomizations(): void {
		if ( get_current_user_id() === 0 ) {
			return;
		}

		if ( wp_doing_cron() ) {
			return;
		}

		if ( wp_doing_ajax() ) {
			$action = isset( $_REQUEST['action'] ) ? sanitize_key( (string) $_REQUEST['action'] ) : '';
			if ( in_array( $action, [ 'heartbeat', 'wp-compression-test' ], true ) ) {
				return;
			}
		}

		$settings = $this->getSettings();
		$menu_config = $settings['menu_config'] ?? null;

		if ( empty( $menu_config ) || ! is_array( $menu_config ) ) {
			return;
		}

		global $menu, $submenu;

		if ( ! is_array( $menu ) || empty( $menu ) ) {
			return;
		}

		$menu_lookup = [];
		$config_slugs = [];
		
		foreach ( $menu as $key => $item ) {
			$slug = $item[2] ?? '';
			if ( $slug ) {
				$menu_lookup[ $slug ] = $key;
			}
		}

		foreach ( $menu_config as $item ) {
			if ( ! empty( $item['is_separator'] ) ) {
				continue;
			}
			$slug = $this->getItemSlug( $item );
			if ( $slug !== '' ) {
				$config_slugs[ $slug ] = $item;
			}
		}

		$new_menu           = [];
		$new_submenu        = [];
		$applied_top_slugs  = [];
		$menu_order         = 0;

		// Apply saved order first (config array order, not stale position integers).
		foreach ( $menu_config as $item ) {
			$is_separator  = ! empty( $item['is_separator'] );
			$menu_slug     = $item['menu_slug'] ?? '';
			$resolved_slug = $this->getItemSlug( $item );
			$parent_slug   = ! empty( $item['parent_slug'] ) ? $this->sanitizeMenuSlug( (string) $item['parent_slug'] ) : '';

			if ( $this->shouldRemoveMenuItemForCurrentUser( $item ) ) {
				continue;
			}

			if ( $is_separator ) {
				$separator_slug = ! empty( $menu_slug ) ? $this->sanitizeMenuSlug( (string) $menu_slug ) : 'separator-' . $menu_order;
				$new_menu[ $menu_order++ ] = [
					'',
					'read',
					$separator_slug,
					'',
					'menu-top menu-icon-separator',
					'menu-separator-' . $menu_order,
					'',
				];
				continue;
			}

			if ( $parent_slug !== '' && $parent_slug !== $resolved_slug ) {
				continue;
			}

			if ( ! empty( $item['is_custom'] ) && ! $this->userCanSeeCustomItem( $item ) ) {
				continue;
			}

			if ( $resolved_slug === '' || isset( $applied_top_slugs[ $resolved_slug ] ) ) {
				continue;
			}

			$lookup_slug = null;
			if ( isset( $menu_lookup[ $resolved_slug ] ) ) {
				$lookup_slug = $resolved_slug;
			} elseif ( $menu_slug !== '' && isset( $menu_lookup[ $menu_slug ] ) ) {
				$lookup_slug = $menu_slug;
			}

			if ( $lookup_slug === null ) {
				continue;
			}

			$original_key = $menu_lookup[ $lookup_slug ];
			$menu_item    = $menu[ $original_key ];

			if ( ! empty( $item['title'] ) ) {
				$menu_item[0] = $this->mergeMenuTitleBubbles( $menu_item[0] ?? '', $item['title'] );
			}

			if ( ! empty( $item['icon'] ) ) {
				$menu_item[6] = $item['icon'];
			}

			$new_menu[ $menu_order++ ]        = $menu_item;
			$applied_top_slugs[ $resolved_slug ] = true;

			$submenu_parent_key = $lookup_slug;
			if ( ! empty( $item['submenu'] ) && is_array( $item['submenu'] ) ) {
				$visited_apply_slugs = [];
				$this->applyConfiguredSubmenu( $submenu_parent_key, $item['submenu'], $submenu, $new_submenu, $visited_apply_slugs );
			} elseif ( isset( $submenu[ $submenu_parent_key ] ) ) {
				$new_submenu[ $submenu_parent_key ] = $submenu[ $submenu_parent_key ];
				$visited_copy_slugs                 = [];
				$this->copyNestedSubmenuBranches( $submenu_parent_key, $submenu, $new_submenu, $visited_copy_slugs );
			}
		}

		// Append WP items not in saved config (e.g. newly installed plugins).
		foreach ( $menu as $key => $menu_item ) {
			$slug         = $menu_item[2] ?? '';
			$is_separator = empty( $slug ) || $slug === 'separator' || strpos( $slug, 'separator' ) === 0;

			if ( $is_separator ) {
				continue;
			}

			if ( isset( $applied_top_slugs[ $slug ] ) ) {
				continue;
			}

			$config_item = $config_slugs[ $slug ] ?? null;
			if ( $config_item && $this->shouldRemoveMenuItemForCurrentUser( $config_item ) ) {
				continue;
			}

			if ( $config_item ) {
				$item_parent = ! empty( $config_item['parent_slug'] ) ? $this->sanitizeMenuSlug( (string) $config_item['parent_slug'] ) : '';
				if ( $item_parent !== '' && $item_parent !== $slug ) {
					continue;
				}
				continue;
			}

			$new_menu[ $menu_order++ ] = $menu_item;
			$applied_top_slugs[ $slug ] = true;

			if ( isset( $submenu[ $slug ] ) ) {
				$new_submenu[ $slug ] = $submenu[ $slug ];
			}
		}

		// Attach items relocated under another parent (separate config rows with parent_slug).
		foreach ( $menu_config as $item ) {
			if ( ! empty( $item['is_separator'] ) ) {
				continue;
			}

			$parent_slug = ! empty( $item['parent_slug'] ) ? $this->sanitizeMenuSlug( (string) $item['parent_slug'] ) : '';
			if ( $parent_slug === '' || $this->shouldRemoveMenuItemForCurrentUser( $item ) ) {
				continue;
			}

			$resolved_slug = $this->getItemSlug( $item );
			$menu_slug     = $item['menu_slug'] ?? '';
			if ( $resolved_slug === '' || $parent_slug === $resolved_slug ) {
				continue;
			}

			if ( ! empty( $item['is_custom'] ) ) {
				if ( ! $this->userCanSeeCustomItem( $item ) ) {
					continue;
				}
				$row = [
					trim( wp_strip_all_tags( $item['title'] ?? '' ) ),
					$item['capability'] ?? 'read',
					$resolved_slug,
					$item['page_title'] ?? $item['title'] ?? '',
				];
				$this->appendSubmenuRow( $new_submenu, $parent_slug, $row );
				continue;
			}

			$lookup_slug = isset( $menu_lookup[ $menu_slug ] ) ? $menu_slug : null;
			if ( $lookup_slug === null ) {
				continue;
			}

			$menu_item = $menu[ $menu_lookup[ $lookup_slug ] ];
			if ( ! empty( $item['title'] ) ) {
				$menu_item[0] = $this->mergeMenuTitleBubbles( $menu_item[0] ?? '', $item['title'] );
			}

			$row = [
				$menu_item[0],
				$menu_item[1],
				$menu_slug,
				$menu_item[3] ?? $menu_item[0],
			];
			$this->appendSubmenuRow( $new_submenu, $parent_slug, $row );
		}

		$soft_hidden_count = 0;
		foreach ( $new_menu as $pos => &$menu_row ) {
			$slug = $menu_row[2] ?? '';
			if ( $slug === '' || ! isset( $config_slugs[ $slug ] ) ) {
				continue;
			}
			$config_item = $config_slugs[ $slug ];
			if ( $this->isHiddenForCurrentUser( $config_item ) && $this->usesSoftHideBehavior( $config_item ) ) {
				$menu_row = $this->applySoftHideMenuClasses( $menu_row );
				$soft_hidden_count++;
			}
		}
		unset( $menu_row );

		if ( $soft_hidden_count > 0 ) {
			$new_menu[ $menu_order++ ] = [
				sprintf(
					/* translators: %d: number of collapsed menu items */
					__( 'Show All (%d)', 'wp-powersuite' ),
					$soft_hidden_count
				),
				'read',
				'#wpps-show-all-menu',
				'',
				'wpps-show-all-toggle menu-top',
				'wpps-show-all-menu',
				'dashicons-arrow-down-alt2',
			];
		}

		$menu = array_values( $new_menu );

		foreach ( $new_submenu as $parent_slug => $submenu_items ) {
			$submenu[ $parent_slug ] = $this->pruneSelfReferencingSubmenuDuplicates( $parent_slug, $submenu_items );
		}
	}

	/**
	 * WordPress adds a first submenu row matching the parent slug after add_menu_page().
	 * Remove that duplicate when the parent has other submenu entries (e.g. moved custom items).
	 *
	 * @param array<int, array<int, string>> $submenu_items
	 * @return array<int, array<int, string>>
	 */
	private function pruneSelfReferencingSubmenuDuplicates( string $parent_slug, array $submenu_items ): array {
		if ( count( $submenu_items ) < 2 || ! $this->isWppsCustomMenuSlug( $parent_slug ) ) {
			return $submenu_items;
		}

		$pruned = [];
		foreach ( $submenu_items as $row ) {
			if ( ( $row[2] ?? '' ) === $parent_slug ) {
				continue;
			}
			$pruned[] = $row;
		}

		return ! empty( $pruned ) ? array_values( $pruned ) : $submenu_items;
	}

	private function isWppsCustomMenuSlug( string $slug ): bool {
		return str_starts_with( $slug, 'wpps-link-' ) || str_starts_with( $slug, 'wpps-custom-' );
	}


	public function handleGetMenuConfig(): void {
		check_ajax_referer( 'wpps_menu_organizer', 'nonce' );

		if ( ! $this->canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'wp-powersuite' ) ] );
		}

		$settings    = $this->getSettings();
		$menu_config = $settings['menu_config'] ?? null;

		if ( empty( $menu_config ) || ! is_array( $menu_config ) ) {
			$menu_config = $this->getCurrentMenu();
		} else {
			$menu_config = $this->getEditorMenuConfig( $menu_config );
		}

		$users_map = $this->buildUsersMapFromConfig( $menu_config );
		wp_send_json_success( [ 'menu_config' => $menu_config, 'users_map' => $users_map ] );
	}

	private function buildUsersMapFromConfig( array $config ): object {
		$user_ids = [];
		foreach ( $config as $item ) {
			$hide_rules = $item['hide_rules'] ?? null;
			if ( ! empty( $hide_rules['users'] ) && is_array( $hide_rules['users'] ) ) {
				$user_ids = array_merge( $user_ids, $hide_rules['users'] );
			}
			$this->collectHideRuleUserIdsFromSubmenu( $item['submenu'] ?? [], $user_ids );
		}
		$user_ids = array_unique( array_filter( array_map( 'absint', $user_ids ) ) );
		$map      = new \stdClass();
		if ( ! empty( $user_ids ) ) {
			$users = get_users( [ 'include' => $user_ids, 'fields' => [ 'ID', 'user_login', 'display_name' ] ] );
			foreach ( $users as $u ) {
				$map->{$u->ID} = [
					'display_name' => $u->display_name,
					'user_login'   => $u->user_login,
				];
			}
		}
		return $map;
	}

	public function handleSaveMenuConfig(): void {
		check_ajax_referer( 'wpps_menu_organizer', 'nonce' );

		if ( ! $this->canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'wp-powersuite' ) ] );
		}

		$raw_config = isset( $_POST['menu_config'] ) ? wp_unslash( $_POST['menu_config'] ) : '';
		if ( ! is_string( $raw_config ) || $raw_config === '' ) {
			wp_send_json_error( [ 'message' => __( 'Invalid menu configuration.', 'wp-powersuite' ) ] );
		}

		if ( strlen( $raw_config ) > self::MAX_MENU_CONFIG_BYTES ) {
			wp_send_json_error( [ 'message' => __( 'Menu configuration is too large to save.', 'wp-powersuite' ) ] );
		}

		$menu_config = json_decode( $raw_config, true );

		if ( ! is_array( $menu_config ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid menu configuration.', 'wp-powersuite' ) ] );
		}

		if ( $this->countMenuConfigItems( $menu_config ) > self::MAX_MENU_CONFIG_ITEMS ) {
			wp_send_json_error( [ 'message' => __( 'Menu configuration has too many items.', 'wp-powersuite' ) ] );
		}

		$menu_config = $this->sanitizeMenuConfig( $menu_config );

		try {
			$this->saveMenuConfig( $menu_config );
			wp_send_json_success( [ 'message' => __( 'Menu configuration saved successfully!', 'wp-powersuite' ) ] );
		} catch ( \Exception $e ) {
			$this->logHandlerException( 'WPPS Admin Menu Organizer save error', $e );
			wp_send_json_error( [ 'message' => __( 'Could not save configuration. Please try again.', 'wp-powersuite' ) ] );
		}
	}

	public function handleRestoreMenuConfig(): void {
		check_ajax_referer( 'wpps_menu_organizer', 'nonce' );

		if ( ! $this->canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'wp-powersuite' ) ] );
		}

		try {
			$this->deleteMenuConfig();
			wp_send_json_success( [ 'message' => __( 'Menu restored to defaults successfully!', 'wp-powersuite' ) ] );
		} catch ( \Exception $e ) {
			$this->logHandlerException( 'WPPS Admin Menu Organizer restore error', $e );
			wp_send_json_error( [ 'message' => __( 'Could not restore configuration. Please try again.', 'wp-powersuite' ) ] );
		}
	}

	private function getCurrentMenu(): array {
		global $menu, $submenu;
		
		if ( ! is_array( $menu ) || empty( $menu ) ) {
			return [];
		}

		$menu_structure = [];
		$role_names     = wp_roles()->get_names();
		$role_objects   = [];
		foreach ( array_keys( $role_names ) as $role_key ) {
			$role = get_role( $role_key );
			if ( $role ) {
				$role_objects[ $role_key ] = $role;
			}
		}
		$eligible_roles_cache = [];

		foreach ( $menu as $menu_key => $menu_item ) {
			$menu_slug = $menu_item[2] ?? '';
			$is_separator = empty( $menu_slug ) || $menu_slug === 'separator' || strpos( $menu_slug, 'separator' ) === 0;
			
			if ( $is_separator ) {
				continue;
			}

			$title = $menu_item[0] ?? '';
			$title = preg_replace( '/<span[^>]*class=["\'][^"\']*awaiting-mod[^"\']*["\'][^>]*>.*?<\/span>/i', '', $title );
			$title = preg_replace( '/<span[^>]*class=["\'][^"\']*update-plugins[^"\']*["\'][^>]*>.*?<\/span>/i', '', $title );
			$title = strip_tags( $title );
			$title = preg_replace( '/\s*0\s+Comments\s+in\s+moderation\s*/i', '', $title );
			$title = preg_replace( '/\s*\d+\s+Comments\s+in\s+moderation\s*/i', '', $title );
			$title = trim( $title );
			
			$capability = $menu_item[1] ?? 'read';
			if ( ! isset( $eligible_roles_cache[ $capability ] ) ) {
				$eligible_roles_cache[ $capability ] = $this->getEligibleRolesForCapability( $capability, $role_names, $role_objects );
			}
			$eligible_roles = $eligible_roles_cache[ $capability ];
			
			$item_data = [
				'menu_key'      => $menu_key,
				'title'         => $title,
				'capability'    => $capability,
				'menu_slug'     => $menu_slug,
				'page_title'    => $menu_item[3] ?? '',
				'icon'          => $menu_item[6] ?? '',
				'position'      => $menu_key,
				'parent_slug'   => '',
				'hidden'        => false,
				'is_separator'  => $is_separator,
				'eligible_roles' => $eligible_roles,
				'submenu'       => [],
			];

			$visited_submenu_slugs = [];
			$item_data['submenu']  = $this->buildSubmenuConfigFromGlobals(
				$menu_slug,
				$role_names,
				$role_objects,
				$eligible_roles_cache,
				$visited_submenu_slugs
			);

			$menu_structure[] = $item_data;
		}

		return $menu_structure;
	}

	/**
	 * Merge saved organizer config with the live admin menu (includes nested submenu branches).
	 *
	 * @param array<int, array<string, mixed>>|null $saved_menu_config
	 * @return array<int, array<string, mixed>>
	 */
	public function getEditorMenuConfig( ?array $saved_menu_config ): array {
		$current_menu_for_js = $this->getCurrentMenu();

		if ( empty( $saved_menu_config ) || ! is_array( $saved_menu_config ) ) {
			return $current_menu_for_js;
		}

		$menu_config = $saved_menu_config;

		$saved_config_lookup = [];
		foreach ( $saved_menu_config as $saved_item ) {
			$saved_slug = $this->getItemSlug( $saved_item );
			if ( $saved_slug !== '' ) {
				$saved_config_lookup[ $saved_slug ] = $saved_item;
			}
		}

		$current_menu_lookup = [];
		foreach ( $current_menu_for_js as $current_item ) {
			$current_slug = $this->getItemSlug( $current_item );
			if ( $current_slug !== '' ) {
				$current_menu_lookup[ $current_slug ] = $current_item;
			}
		}

		$relocated_by_parent = $this->getRelocatedChildSlugsByParent( $saved_menu_config );

		foreach ( $menu_config as $index => $saved_item ) {
			$saved_slug = $this->getItemSlug( $saved_item );
			if ( $saved_slug === '' || ! isset( $current_menu_lookup[ $saved_slug ] ) ) {
				continue;
			}

			$current_item = $current_menu_lookup[ $saved_slug ];
			if ( isset( $current_item['eligible_roles'] ) ) {
				$menu_config[ $index ]['eligible_roles'] = $current_item['eligible_roles'];
			}

			$exclude_relocated = $relocated_by_parent[ $saved_slug ] ?? [];

			if (
				isset( $current_item['submenu'], $saved_item['submenu'] )
				&& is_array( $current_item['submenu'] )
				&& is_array( $saved_item['submenu'] )
			) {
				$menu_config[ $index ]['submenu'] = $this->mergeSavedSubmenuWithCurrent(
					$saved_item['submenu'],
					$current_item['submenu'],
					$exclude_relocated
				);
			} elseif ( ! empty( $current_item['submenu'] ) && is_array( $current_item['submenu'] ) ) {
				$menu_config[ $index ]['submenu'] = $this->mergeSavedSubmenuWithCurrent(
					$saved_item['submenu'] ?? [],
					$current_item['submenu'],
					$exclude_relocated
				);
			}
		}

		foreach ( $current_menu_for_js as $current_item ) {
			$current_slug = $this->getItemSlug( $current_item );
			if ( $current_slug !== '' && ! isset( $saved_config_lookup[ $current_slug ] ) ) {
				$menu_config[] = $current_item;
			}
		}

		return $this->normalizeMenuConfigPositions( $menu_config );
	}

	/**
	 * @param array<int, array<string, mixed>> $saved_submenu
	 * @param array<int, array<string, mixed>> $current_submenu
	 * @return array<int, array<string, mixed>>
	 */
	/**
	 * Top-level config rows with parent_slug (Move under) — keyed by parent slug.
	 *
	 * @param array<int, array<string, mixed>> $menu_config
	 * @return array<string, array<string, true>>
	 */
	private function getRelocatedChildSlugsByParent( array $menu_config ): array {
		$by_parent = [];

		foreach ( $menu_config as $item ) {
			if ( ! empty( $item['is_separator'] ) ) {
				continue;
			}

			$parent = ! empty( $item['parent_slug'] ) ? $this->sanitizeMenuSlug( (string) $item['parent_slug'] ) : '';
			$slug   = $this->getItemSlug( $item );
			if ( $parent === '' || $slug === '' || $parent === $slug ) {
				continue;
			}

			if ( ! isset( $by_parent[ $parent ] ) ) {
				$by_parent[ $parent ] = [];
			}
			$by_parent[ $parent ][ $slug ] = true;
		}

		return $by_parent;
	}

	/**
	 * @param array<int, array<string, mixed>> $submenu
	 * @param array<string, true>              $exclude_slugs
	 * @return array<int, array<string, mixed>>
	 */
	private function filterSubmenuExcludingSlugs( array $submenu, array $exclude_slugs ): array {
		if ( empty( $exclude_slugs ) ) {
			return $submenu;
		}

		$filtered = [];
		foreach ( $submenu as $sub ) {
			$slug = $this->getItemSlug( $sub );
			if ( $slug !== '' && isset( $exclude_slugs[ $slug ] ) ) {
				continue;
			}
			$filtered[] = $sub;
		}

		return $filtered;
	}

	/**
	 * @param array<int, array<string, mixed>> $saved_submenu
	 * @param array<int, array<string, mixed>> $current_submenu
	 * @param array<string, true>              $exclude_slugs Slugs shown via parent_slug, not native submenu merge.
	 * @return array<int, array<string, mixed>>
	 */
	private function mergeSavedSubmenuWithCurrent( array $saved_submenu, array $current_submenu, array $exclude_slugs = [] ): array {
		$current_submenu = $this->filterSubmenuExcludingSlugs( $current_submenu, $exclude_slugs );

		$current_lookup = [];
		foreach ( $current_submenu as $current_sub ) {
			$slug = $this->getItemSlug( $current_sub );
			if ( $slug !== '' ) {
				$current_lookup[ $slug ] = $current_sub;
			}
		}

		$merged = [];
		foreach ( $saved_submenu as $saved_sub ) {
			$slug = $this->getItemSlug( $saved_sub );
			if ( $slug !== '' && isset( $current_lookup[ $slug ] ) ) {
				$current_sub = $current_lookup[ $slug ];
				if ( isset( $current_sub['eligible_roles'] ) ) {
					$saved_sub['eligible_roles'] = $current_sub['eligible_roles'];
				}
				if (
					isset( $current_sub['submenu'], $saved_sub['submenu'] )
					&& is_array( $current_sub['submenu'] )
					&& is_array( $saved_sub['submenu'] )
				) {
					$saved_sub['submenu'] = $this->mergeSavedSubmenuWithCurrent(
						$saved_sub['submenu'],
						$current_sub['submenu']
					);
				} elseif ( ! empty( $current_sub['submenu'] ) && is_array( $current_sub['submenu'] ) ) {
					$saved_sub['submenu'] = $this->mergeSavedSubmenuWithCurrent(
						$saved_sub['submenu'] ?? [],
						$current_sub['submenu']
					);
				} elseif ( empty( $saved_sub['submenu'] ) && ! empty( $current_sub['submenu'] ) ) {
					$saved_sub['submenu'] = $current_sub['submenu'];
				}
			}

			$merged[] = $saved_sub;
		}

		$saved_slugs = [];
		foreach ( $merged as $sub ) {
			$slug = $this->getItemSlug( $sub );
			if ( $slug !== '' ) {
				$saved_slugs[ $slug ] = true;
			}
		}

		foreach ( $current_submenu as $current_sub ) {
			$slug = $this->getItemSlug( $current_sub );
			if ( $slug !== '' && ! isset( $saved_slugs[ $slug ] ) ) {
				$merged[] = $current_sub;
			}
		}

		return $merged;
	}

	/**
	 * @param array<string, string>              $role_names
	 * @param array<string, \WP_Role>            $role_objects
	 * @param array<string, array<string, string>> $eligible_roles_cache
	 * @return array<int, array<string, mixed>>
	 */
	/**
	 * Whether $submenu[ $child_slug ] should be loaded (avoids self/cyclic WP menu registration).
	 *
	 * @param array<string, true> $visited_slugs
	 */
	private function shouldLoadNestedSubmenuBranch( string $child_slug, string $parent_slug, array $visited_slugs ): bool {
		if ( $child_slug === '' || $child_slug === $parent_slug ) {
			return false;
		}

		return ! isset( $visited_slugs[ $child_slug ] );
	}

	private function buildSubmenuConfigFromGlobals(
		string $parent_slug,
		array $role_names,
		array $role_objects,
		array &$eligible_roles_cache,
		array &$visited_slugs = []
	): array {
		global $submenu;

		if ( $parent_slug === '' || isset( $visited_slugs[ $parent_slug ] ) ) {
			return [];
		}

		if ( ! isset( $submenu[ $parent_slug ] ) || ! is_array( $submenu[ $parent_slug ] ) ) {
			return [];
		}

		$visited_slugs[ $parent_slug ] = true;

		$items = [];
		foreach ( $submenu[ $parent_slug ] as $submenu_key => $submenu_item ) {
			$sub_title      = $this->stripMenuTitleHtml( (string) ( $submenu_item[0] ?? '' ) );
			$sub_capability = $submenu_item[1] ?? 'read';
			if ( ! isset( $eligible_roles_cache[ $sub_capability ] ) ) {
				$eligible_roles_cache[ $sub_capability ] = $this->getEligibleRolesForCapability(
					$sub_capability,
					$role_names,
					$role_objects
				);
			}

			$sub_slug = $submenu_item[2] ?? '';
			$entry    = [
				'submenu_key'    => $submenu_key,
				'title'          => $sub_title,
				'capability'     => $sub_capability,
				'menu_slug'      => $sub_slug,
				'page_title'     => $submenu_item[3] ?? '',
				'hidden'         => false,
				'eligible_roles' => $eligible_roles_cache[ $sub_capability ],
				'submenu'        => [],
			];

			if ( $this->shouldLoadNestedSubmenuBranch( $sub_slug, $parent_slug, $visited_slugs ) ) {
				$entry['submenu'] = $this->buildSubmenuConfigFromGlobals(
					$sub_slug,
					$role_names,
					$role_objects,
					$eligible_roles_cache,
					$visited_slugs
				);
			}

			$items[] = $entry;
		}

		return $items;
	}

	private function stripMenuTitleHtml( string $title ): string {
		$title = preg_replace( '/<span[^>]*class=["\'][^"\']*awaiting-mod[^"\']*["\'][^>]*>.*?<\/span>/i', '', $title );
		$title = preg_replace( '/<span[^>]*class=["\'][^"\']*update-plugins[^"\']*["\'][^>]*>.*?<\/span>/i', '', $title );
		$title = strip_tags( $title );
		$title = preg_replace( '/\s*0\s+Comments\s+in\s+moderation\s*/i', '', $title );
		$title = preg_replace( '/\s*\d+\s+Comments\s+in\s+moderation\s*/i', '', $title );

		return trim( $title );
	}

	/**
	 * @param array<int, array<string, mixed>> $submenu_items
	 * @param list<int>                        $user_ids
	 */
	private function collectHideRuleUserIdsFromSubmenu( array $submenu_items, array &$user_ids ): void {
		foreach ( $submenu_items as $sub ) {
			$sub_rules = $sub['hide_rules'] ?? null;
			if ( ! empty( $sub_rules['users'] ) && is_array( $sub_rules['users'] ) ) {
				$user_ids = array_merge( $user_ids, $sub_rules['users'] );
			}
			if ( ! empty( $sub['submenu'] ) && is_array( $sub['submenu'] ) ) {
				$this->collectHideRuleUserIdsFromSubmenu( $sub['submenu'], $user_ids );
			}
		}
	}

	/**
	 * @param array<int, array<string, mixed>>     $config_items
	 * @param array<string, array<int, array>>     $original_submenu
	 * @param array<string, array<int, array>>     $new_submenu
	 */
	private function applyConfiguredSubmenu(
		string $wp_parent_slug,
		array $config_items,
		array $original_submenu,
		array &$new_submenu,
		array &$visited_slugs = []
	): void {
		if ( $wp_parent_slug === '' || isset( $visited_slugs[ $wp_parent_slug ] ) ) {
			return;
		}

		$visited_slugs[ $wp_parent_slug ] = true;

		$new_submenu[ $wp_parent_slug ] = [];
		$config_sub_slugs               = [];

		foreach ( $config_items as $sub_item ) {
			if ( $this->shouldRemoveMenuItemForCurrentUser( $sub_item ) ) {
				continue;
			}

			$submenu_slug = $sub_item['menu_slug'] ?? '';
			if ( $submenu_slug === '' ) {
				continue;
			}

			if ( ! empty( $sub_item['is_custom'] ) ) {
				$sub_slug = $this->resolveCustomMenuSlug( $sub_item );
				$row      = [
					trim( wp_strip_all_tags( $sub_item['title'] ?? '' ) ),
					$sub_item['capability'] ?? 'read',
					$sub_slug,
					$sub_item['page_title'] ?? $sub_item['title'] ?? '',
				];
				$this->appendSubmenuRow(
					$new_submenu,
					$wp_parent_slug,
					$this->maybeApplySoftHideSubmenuClasses( $row, $sub_item )
				);
				$config_sub_slugs[] = $sub_slug;

				if ( ! empty( $sub_item['submenu'] ) && is_array( $sub_item['submenu'] ) && $this->shouldLoadNestedSubmenuBranch( $sub_slug, $wp_parent_slug, $visited_slugs ) ) {
					$this->applyConfiguredSubmenu( $sub_slug, $sub_item['submenu'], $original_submenu, $new_submenu, $visited_slugs );
				}
				continue;
			}

			if ( isset( $original_submenu[ $wp_parent_slug ] ) ) {
				foreach ( $original_submenu[ $wp_parent_slug ] as $submenu_item ) {
					if ( ( $submenu_item[2] ?? '' ) !== $submenu_slug ) {
						continue;
					}

					if ( ! empty( $sub_item['title'] ) ) {
						$submenu_item[0] = $this->mergeMenuTitleBubbles( $submenu_item[0] ?? '', $sub_item['title'] );
					}
					$this->appendSubmenuRow(
						$new_submenu,
						$wp_parent_slug,
						$this->maybeApplySoftHideSubmenuClasses( $submenu_item, $sub_item )
					);
					$config_sub_slugs[] = $submenu_slug;

					if ( ! empty( $sub_item['submenu'] ) && is_array( $sub_item['submenu'] ) && $this->shouldLoadNestedSubmenuBranch( $submenu_slug, $wp_parent_slug, $visited_slugs ) ) {
						$this->applyConfiguredSubmenu( $submenu_slug, $sub_item['submenu'], $original_submenu, $new_submenu, $visited_slugs );
					}
					break;
				}
			}
		}

		if ( isset( $original_submenu[ $wp_parent_slug ] ) ) {
			foreach ( $original_submenu[ $wp_parent_slug ] as $submenu_item ) {
				$orig_slug = $submenu_item[2] ?? '';
				if ( $orig_slug !== '' && ! in_array( $orig_slug, $config_sub_slugs, true ) ) {
					$this->appendSubmenuRow( $new_submenu, $wp_parent_slug, $submenu_item );
					if ( $this->shouldLoadNestedSubmenuBranch( $orig_slug, $wp_parent_slug, $visited_slugs ) ) {
						$this->copyNestedSubmenuBranches( $orig_slug, $original_submenu, $new_submenu, $visited_slugs );
					}
				}
			}
		}
	}

	/**
	 * Preserve nested $submenu branches keyed by child slugs (plugin flyout menus).
	 *
	 * @param array<string, array<int, array>> $source
	 * @param array<string, array<int, array>> $target
	 */
	private function copyNestedSubmenuBranches( string $parent_slug, array $source, array &$target, array &$visited_slugs = [] ): void {
		if ( $parent_slug === '' || isset( $visited_slugs[ $parent_slug ] ) ) {
			return;
		}

		if ( ! isset( $source[ $parent_slug ] ) || ! is_array( $source[ $parent_slug ] ) ) {
			return;
		}

		$visited_slugs[ $parent_slug ] = true;

		foreach ( $source[ $parent_slug ] as $submenu_item ) {
			$child_slug = $submenu_item[2] ?? '';
			if ( $child_slug === '' || ! isset( $source[ $child_slug ] ) ) {
				continue;
			}
			if ( ! $this->shouldLoadNestedSubmenuBranch( $child_slug, $parent_slug, $visited_slugs ) ) {
				continue;
			}
			if ( ! isset( $target[ $child_slug ] ) ) {
				$target[ $child_slug ] = $source[ $child_slug ];
			}
			$this->copyNestedSubmenuBranches( $child_slug, $source, $target, $visited_slugs );
		}
	}

	/**
	 * @param array<string, string>           $role_names
	 * @param array<string, \WP_Role>         $role_objects
	 * @return array<string, string>
	 */
	private function getEligibleRolesForCapability( string $capability, array $role_names, array $role_objects ): array {
		$eligible_roles = [];
		foreach ( $role_names as $role_key => $role_label ) {
			$role = $role_objects[ $role_key ] ?? null;
			if ( $role && $role->has_cap( $capability ) ) {
				$eligible_roles[ $role_key ] = $role_label;
			}
		}

		return $eligible_roles;
	}

	/**
	 * Check if a menu item should be hidden for the current user based on hide_rules.
	 *
	 * @param array $item Menu config item with 'hidden' and optional 'hide_rules'.
	 * @return bool True if the item should be hidden for the current user.
	 */
	private function getHideRulesUserContext(): array {
		if ( $this->hide_rules_user_context !== null ) {
			return $this->hide_rules_user_context;
		}

		$user = wp_get_current_user();
		$this->hide_rules_user_context = [
			'user_id'    => get_current_user_id(),
			'user_roles' => ! empty( $user->roles ) ? $user->roles : [],
		];

		return $this->hide_rules_user_context;
	}

	private function isHiddenForCurrentUser( array $item ): bool {
		if ( empty( $item['hidden'] ) ) {
			return false;
		}

		$hide_rules = $item['hide_rules'] ?? null;
		if ( empty( $hide_rules ) || ! is_array( $hide_rules ) ) {
			return true;
		}

		$type = $hide_rules['type'] ?? 'all';
		if ( $type === 'all' ) {
			return true;
		}

		$users = $hide_rules['users'] ?? [];
		if ( is_array( $users ) && ! empty( $users ) ) {
			if ( in_array( $this->getHideRulesUserContext()['user_id'], array_map( 'intval', $users ), true ) ) {
				return true;
			}
		}

		$roles = $hide_rules['roles'] ?? [];
		if ( ! is_array( $roles ) ) {
			$roles = [];
		}

		$user_roles = $this->getHideRulesUserContext()['user_roles'];
		if ( $type === 'specific' ) {
			return ! empty( array_intersect( $user_roles, $roles ) );
		}
		if ( $type === 'except' ) {
			return empty( array_intersect( $user_roles, $roles ) );
		}

		return true;
	}

	private function shouldRemoveMenuItemForCurrentUser( array $item ): bool {
		return $this->isHiddenForCurrentUser( $item ) && ! $this->usesSoftHideBehavior( $item );
	}

	private function usesSoftHideBehavior( array $item ): bool {
		if ( empty( $item['hidden'] ) ) {
			return false;
		}

		$hide_rules = $item['hide_rules'] ?? null;
		if ( ! is_array( $hide_rules ) ) {
			return false;
		}

		if ( ( $hide_rules['mode'] ?? 'remove' ) !== 'toggle' ) {
			return false;
		}

		if ( ! empty( $hide_rules['users'] ) ) {
			return false;
		}

		$type = $hide_rules['type'] ?? 'all';
		if ( ( $type === 'specific' || $type === 'except' ) && ! empty( $hide_rules['roles'] ) ) {
			return false;
		}

		return true;
	}

	private function userCanSeeCustomItem( array $item ): bool {
		if ( empty( $item['is_custom'] ) ) {
			return true;
		}

		$eligible = $item['eligible_roles'] ?? [];
		if ( ! is_array( $eligible ) || $eligible === [] ) {
			return true;
		}

		$user_roles = $this->getHideRulesUserContext()['user_roles'];

		return ! empty( array_intersect( $user_roles, array_keys( $eligible ) ) );
	}

	/**
	 * @param array<int, string> $menu_row
	 * @return array<int, string>
	 */
	private function applySoftHideMenuClasses( array $menu_row ): array {
		$menu_row[4] = trim( ( $menu_row[4] ?? '' ) . ' wpps-soft-hidden' );

		return $menu_row;
	}

	/**
	 * @param array<int, string> $submenu_row
	 * @return array<int, string>
	 */
	private function maybeApplySoftHideSubmenuClasses( array $submenu_row, array $config_item ): array {
		if ( $this->isHiddenForCurrentUser( $config_item ) && $this->usesSoftHideBehavior( $config_item ) ) {
			$submenu_row[4] = trim( ( $submenu_row[4] ?? '' ) . ' wpps-soft-hidden' );
		}

		return $submenu_row;
	}

	private function getItemSlug( array $item ): string {
		if ( ! empty( $item['is_custom'] ) ) {
			return $this->resolveCustomMenuSlug( $item );
		}

		return $this->sanitizeMenuSlug( (string) ( $item['menu_slug'] ?? '' ) );
	}

	/**
	 * @param array<int, array<int, string>> $new_submenu
	 * @param array<int, string>              $row
	 */
	private function appendSubmenuRow( array &$new_submenu, string $parent_slug, array $row ): void {
		$row_slug = $row[2] ?? '';
		if ( $parent_slug === '' || $row_slug === '' ) {
			return;
		}

		if ( ! isset( $new_submenu[ $parent_slug ] ) ) {
			$new_submenu[ $parent_slug ] = [];
		}

		foreach ( $new_submenu[ $parent_slug ] as $existing ) {
			if ( ( $existing[2] ?? '' ) === $row_slug ) {
				return;
			}
		}

		$new_submenu[ $parent_slug ][] = $row;
	}

	private function mergeMenuTitleBubbles( string $original_title, string $new_label ): string {
		$new_plain = trim( wp_strip_all_tags( $new_label ) );
		if ( $new_plain === '' ) {
			return $original_title;
		}

		if ( ! preg_match( '/<span[^>]*>/i', $original_title ) ) {
			return $new_plain;
		}

		$base_original = trim( wp_strip_all_tags( preg_replace( '/<span[^>]*>.*?<\/span>/is', '', $original_title ) ) );
		if ( $new_plain === $base_original ) {
			return $original_title;
		}

		$bubbles = '';
		if ( preg_match_all( '/<span[^>]*>.*?<\/span>/is', $original_title, $matches ) ) {
			foreach ( $matches[0] as $span_html ) {
				$inner = trim( wp_strip_all_tags( $span_html ) );
				if ( $inner !== '' && stripos( $new_plain, $inner ) !== false ) {
					continue;
				}
				$bubbles .= ' ' . $span_html;
			}
		}

		return $new_plain . $bubbles;
	}

	private function sanitizeMenuSlug( string $slug ): string {
		$slug = trim( wp_strip_all_tags( $slug ) );
		if ( $slug === '' ) {
			return '';
		}

		if ( preg_match( '#^https?://#i', $slug ) ) {
			$clean = esc_url_raw( $slug );

			return is_string( $clean ) ? $clean : '';
		}

		return preg_replace( '/[^a-zA-Z0-9_\-\.\?\=&%]/', '', $slug ) ?? '';
	}

	private function resolveCustomMenuSlug( array $item ): string {
		$url = $this->getCustomMenuTargetUrl( $item );
		if ( $url !== '' ) {
			return 'wpps-link-' . substr( md5( $url ), 0, 12 );
		}

		$slug = (string) ( $item['menu_slug'] ?? '' );
		if ( preg_match( '#^https?://#i', $slug ) ) {
			return 'wpps-link-' . substr( md5( $slug ), 0, 12 );
		}

		$clean = $this->sanitizeMenuSlug( $slug );

		return $clean !== '' ? $clean : 'wpps-custom-' . substr( md5( wp_json_encode( $item ) ), 0, 10 );
	}

	private function getCustomMenuTargetUrl( array $item ): string {
		$url = isset( $item['url'] ) ? (string) $item['url'] : '';
		if ( $url !== '' && filter_var( $url, FILTER_VALIDATE_URL ) ) {
			return esc_url_raw( $url ) ?: '';
		}

		$slug = (string) ( $item['menu_slug'] ?? '' );
		if ( preg_match( '#^https?://#i', $slug ) ) {
			return esc_url_raw( $slug ) ?: '';
		}

		return '';
	}

	public function registerCustomMenuPages(): void {
		$menu_config = $this->getSettings()['menu_config'] ?? null;
		if ( empty( $menu_config ) || ! is_array( $menu_config ) ) {
			return;
		}

		$registered_slugs = [];

		foreach ( $menu_config as $item ) {
			if ( empty( $item['is_custom'] ) ) {
				continue;
			}

			if ( $this->shouldRemoveMenuItemForCurrentUser( $item ) || ! $this->userCanSeeCustomItem( $item ) ) {
				continue;
			}

			$slug        = $this->resolveCustomMenuSlug( $item );
			$url         = $this->getCustomMenuTargetUrl( $item );
			$parent_slug = ! empty( $item['parent_slug'] ) ? $this->sanitizeMenuSlug( (string) $item['parent_slug'] ) : '';

			if ( $slug === '' || isset( $registered_slugs[ $slug ] ) ) {
				continue;
			}

			$registered_slugs[ $slug ] = true;

			$title = trim( wp_strip_all_tags( $item['title'] ?? __( 'Custom Link', 'wp-powersuite' ) ) );
			$cap   = $item['capability'] ?? 'read';
			$icon  = $item['icon'] ?? 'dashicons-admin-generic';
			$callback = static function () use ( $url ): void {
				if ( $url !== '' ) {
					wp_safe_redirect( $url );
					exit;
				}
			};

			if ( $parent_slug !== '' && $parent_slug !== $slug ) {
				add_submenu_page(
					$parent_slug,
					$title,
					$title,
					$cap,
					$slug,
					$callback
				);
				continue;
			}

			add_menu_page(
				$title,
				$title,
				$cap,
				$slug,
				$callback,
				$icon
			);
		}

		foreach ( $menu_config as $parent_item ) {
			if ( empty( $parent_item['submenu'] ) || ! is_array( $parent_item['submenu'] ) ) {
				continue;
			}

			$parent_menu_slug = $this->getItemSlug( $parent_item );
			if ( $parent_menu_slug === '' ) {
				continue;
			}

			$visited_register_slugs = [];
			$this->registerCustomSubmenuTree( $parent_menu_slug, $parent_item['submenu'], $visited_register_slugs, $registered_slugs );
		}
	}

	/**
	 * @param array<int, array<string, mixed>> $submenu_items
	 * @param array<string, true>              $visited_slugs
	 * @param array<string, true>              $registered_slugs
	 */
	private function registerCustomSubmenuTree( string $parent_slug, array $submenu_items, array &$visited_slugs = [], array &$registered_slugs = [] ): void {
		if ( $parent_slug !== '' ) {
			$visited_slugs[ $parent_slug ] = true;
		}

		foreach ( $submenu_items as $sub_item ) {
			if ( empty( $sub_item['is_custom'] ) ) {
				$child_slug = ! empty( $sub_item['menu_slug'] ) ? $this->sanitizeMenuSlug( (string) $sub_item['menu_slug'] ) : '';
				if (
					$child_slug !== ''
					&& ! empty( $sub_item['submenu'] )
					&& is_array( $sub_item['submenu'] )
					&& $this->shouldLoadNestedSubmenuBranch( $child_slug, $parent_slug, $visited_slugs )
				) {
					$this->registerCustomSubmenuTree( $child_slug, $sub_item['submenu'], $visited_slugs, $registered_slugs );
				}
				continue;
			}

			if ( $this->shouldRemoveMenuItemForCurrentUser( $sub_item ) || ! $this->userCanSeeCustomItem( $sub_item ) ) {
				continue;
			}

			$sub_slug = $this->resolveCustomMenuSlug( $sub_item );
			$sub_url  = $this->getCustomMenuTargetUrl( $sub_item );
			if ( $sub_slug === '' || isset( $registered_slugs[ $sub_slug ] ) ) {
				continue;
			}

			$registered_slugs[ $sub_slug ] = true;

			$sub_title = trim( wp_strip_all_tags( $sub_item['title'] ?? __( 'Custom Link', 'wp-powersuite' ) ) );
			$sub_cap   = $sub_item['capability'] ?? 'read';

			add_submenu_page(
				$parent_slug,
				$sub_title,
				$sub_title,
				$sub_cap,
				$sub_slug,
				static function () use ( $sub_url ): void {
					if ( $sub_url !== '' ) {
						wp_safe_redirect( $sub_url );
						exit;
					}
				}
			);

			if (
				! empty( $sub_item['submenu'] )
				&& is_array( $sub_item['submenu'] )
				&& $this->shouldLoadNestedSubmenuBranch( $sub_slug, $parent_slug, $visited_slugs )
			) {
				$this->registerCustomSubmenuTree( $sub_slug, $sub_item['submenu'], $visited_slugs, $registered_slugs );
			}
		}
	}

	public function printAdminMenuOrganizerCss(): void {
		$menu_config = $this->getSettings()['menu_config'] ?? null;
		if ( empty( $menu_config ) || ! is_array( $menu_config ) ) {
			return;
		}
		?>
		<style>
			#adminmenu .wpps-soft-hidden { display: none !important; }
			#adminmenu.wpps-show-hidden .wpps-soft-hidden { display: block !important; }
			#adminmenu .wpps-show-all-toggle { border-top: 1px solid rgba(255,255,255,.08); margin-top: 4px; }
			#adminmenu .wpps-show-all-toggle > a { font-style: italic; }
		</style>
		<?php
	}

	public function maybeRestrictScreenAccess(): void {
		if ( ! is_admin() || ! is_user_logged_in() ) {
			return;
		}

		$menu_config = $this->getSettings()['menu_config'] ?? null;
		if ( empty( $menu_config ) || ! is_array( $menu_config ) ) {
			return;
		}

		$page_id = $this->getCurrentAdminPageUrlFragment();
		if ( $page_id === '' ) {
			return;
		}

		if ( $this->menuConfigBlocksPageAccess( $menu_config, $page_id ) ) {
			wp_die( esc_html__( 'Sorry, you are not allowed to access this page.', 'wp-powersuite' ), '', [ 'response' => 403 ] );
		}
	}

	/**
	 * @param array<int, array<string, mixed>> $menu_config
	 */
	private function menuConfigBlocksPageAccess( array $menu_config, string $page_id ): bool {
		foreach ( $menu_config as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			if ( $this->menuItemBlocksPageAccess( $page_id, $item ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param array<string, mixed> $item
	 */
	private function menuItemBlocksPageAccess( string $page_id, array $item ): bool {
		if ( $this->pageMatchesMenuItem( $page_id, $item ) ) {
			if ( $this->shouldRemoveMenuItemForCurrentUser( $item ) && $this->isHiddenForCurrentUser( $item ) ) {
				return true;
			}
		}

		if ( ! empty( $item['submenu'] ) && is_array( $item['submenu'] ) ) {
			foreach ( $item['submenu'] as $sub_item ) {
				if ( is_array( $sub_item ) && $this->menuItemBlocksPageAccess( $page_id, $sub_item ) ) {
					return true;
				}
			}
		}

		return false;
	}

	private function getCurrentAdminPageUrlFragment(): string {
		global $wp;

		$current = add_query_arg( $wp->query_vars ?? [] );

		if ( $current === '/wp-admin/' || $current === '/wp-admin/index.php' ) {
			return 'index.php';
		}

		if ( str_contains( $current, '?page=' ) ) {
			$parts = explode( '?page=', $current, 2 );

			return $parts[1] ?? '';
		}

		return ltrim( str_replace( '/wp-admin/', '', $current ), '/' );
	}

	private function pageMatchesMenuItem( string $page_id, array $item ): bool {
		$slug = $this->getItemSlug( $item );

		return $slug !== '' && $page_id === $slug;
	}

	public function handleSearchUsers(): void {
		check_ajax_referer( 'wpps_menu_organizer', 'nonce' );
		if ( ! $this->canManagePlugin() || ! current_user_can( 'list_users' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'wp-powersuite' ) ] );
		}

		$term = isset( $_POST['term'] ) ? sanitize_text_field( wp_unslash( $_POST['term'] ) ) : '';
		if ( strlen( $term ) < 2 ) {
			wp_send_json_success( [ 'users' => [] ] );
		}

		$query = new \WP_User_Query( [
			'search'         => '*' . $term . '*',
			'search_columns' => [ 'user_login', 'user_email', 'display_name' ],
			'number'         => 15,
			'orderby'        => 'display_name',
			'order'          => 'ASC',
			'fields'         => [ 'ID', 'user_login', 'display_name' ],
		] );

		$seen_ids = [];
		$results  = [];
		foreach ( $query->get_results() as $user ) {
			$id = (int) $user->ID;
			if ( isset( $seen_ids[ $id ] ) ) {
				continue;
			}
			$seen_ids[ $id ] = true;
			$results[]      = [
				'id'           => $id,
				'display_name' => $user->display_name,
				'user_login'   => $user->user_login,
			];
		}

		wp_send_json_success( [ 'users' => $results ] );
	}

	private function sanitizeMenuConfig( array $config ): array {
		foreach ( $config as $key => $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$config[ $key ] = $this->sanitizeMenuConfigItem( $item );
		}

		$config = $this->dedupeMenuConfig( $config );

		return $this->normalizeMenuConfigPositions( $config );
	}

	/**
	 * @param array<int, array<string, mixed>> $config
	 * @return array<int, array<string, mixed>>
	 */
	private function dedupeMenuConfig( array $config ): array {
		$seen   = [];
		$result = [];

		foreach ( $config as $item ) {
			$slug = $this->getItemSlug( $item );
			if ( $slug !== '' && isset( $seen[ $slug ] ) ) {
				continue;
			}
			if ( $slug !== '' ) {
				$seen[ $slug ] = true;
			}
			if ( ! empty( $item['submenu'] ) && is_array( $item['submenu'] ) ) {
				$item['submenu'] = $this->dedupeSubmenuConfig( $item['submenu'] );
			}
			$result[] = $item;
		}

		return $result;
	}

	/**
	 * @param array<int, array<string, mixed>> $submenu
	 * @return array<int, array<string, mixed>>
	 */
	private function dedupeSubmenuConfig( array $submenu ): array {
		$seen   = [];
		$result = [];

		foreach ( $submenu as $sub ) {
			$slug = $this->getItemSlug( $sub );
			if ( $slug !== '' && isset( $seen[ $slug ] ) ) {
				continue;
			}
			if ( $slug !== '' ) {
				$seen[ $slug ] = true;
			}
			if ( ! empty( $sub['submenu'] ) && is_array( $sub['submenu'] ) ) {
				$sub['submenu'] = $this->dedupeSubmenuConfig( $sub['submenu'] );
			}
			$result[] = $sub;
		}

		return $result;
	}

	/**
	 * @param array<int, array<string, mixed>> $config
	 * @return array<int, array<string, mixed>>
	 */
	private function normalizeMenuConfigPositions( array $config ): array {
		foreach ( $config as $index => &$item ) {
			$item['position'] = $index;
			if ( ! empty( $item['submenu'] ) && is_array( $item['submenu'] ) ) {
				foreach ( $item['submenu'] as $sub_index => &$sub ) {
					$sub['position'] = $sub_index;
				}
				unset( $sub );
			}
		}
		unset( $item );

		return $config;
	}

	private function sanitizeMenuConfigItem( array $item ): array {
		if ( isset( $item['parent_slug'] ) ) {
			$item['parent_slug'] = $this->sanitizeMenuSlug( (string) $item['parent_slug'] );
		}
		if ( isset( $item['title'] ) ) {
			$item['title'] = sanitize_text_field( $item['title'] );
		}
		if ( isset( $item['page_title'] ) ) {
			$item['page_title'] = sanitize_text_field( $item['page_title'] );
		}
		if ( isset( $item['menu_slug'] ) ) {
			$item['menu_slug'] = $this->sanitizeMenuSlug( (string) $item['menu_slug'] );
		}
		if ( isset( $item['url'] ) ) {
			$url = (string) $item['url'];
			$item['url'] = preg_match( '#^https?://#i', $url ) ? ( esc_url_raw( $url ) ?: '' ) : '';
		}
		if ( isset( $item['icon'] ) ) {
			$item['icon'] = $this->sanitizeMenuIcon( (string) $item['icon'] );
		}
		if ( isset( $item['capability'] ) ) {
			$item['capability'] = $this->sanitizeMenuCapability( (string) $item['capability'] );
		}
		if ( ! empty( $item['hide_rules'] ) && is_array( $item['hide_rules'] ) ) {
			$mode = $item['hide_rules']['mode'] ?? 'remove';
			$item['hide_rules']['mode'] = in_array( $mode, [ 'toggle', 'remove' ], true ) ? $mode : 'remove';
			if ( isset( $item['hide_rules']['users'] ) && is_array( $item['hide_rules']['users'] ) ) {
				$item['hide_rules']['users'] = array_values( array_filter( array_map( 'absint', $item['hide_rules']['users'] ) ) );
			}
			if ( isset( $item['hide_rules']['roles'] ) && is_array( $item['hide_rules']['roles'] ) ) {
				$item['hide_rules']['roles'] = array_values( array_filter( array_map( 'sanitize_key', $item['hide_rules']['roles'] ) ) );
			}
		}
		if ( ! empty( $item['submenu'] ) && is_array( $item['submenu'] ) ) {
			foreach ( $item['submenu'] as $sk => $sub ) {
				if ( is_array( $sub ) ) {
					$item['submenu'][ $sk ] = $this->sanitizeMenuConfigItem( $sub );
				}
			}
		}

		if ( ! empty( $item['is_custom'] ) ) {
			$target_url = $this->getCustomMenuTargetUrl( $item );
			if ( $target_url !== '' ) {
				$item['url'] = $target_url;
			}
			$item['menu_slug'] = $this->resolveCustomMenuSlug( $item );
			if ( ! $this->isWppsCustomMenuSlug( $item['menu_slug'] ) ) {
				unset( $item['is_custom'], $item['url'] );
			} else {
				$item['is_custom'] = true;
			}
		} else {
			unset( $item['is_custom'], $item['url'] );
		}

		if ( isset( $item['eligible_roles'] ) && is_array( $item['eligible_roles'] ) ) {
			$item['eligible_roles'] = $this->sanitizeEligibleRoles( $item['eligible_roles'] );
		}

		return $item;
	}

	/**
	 * @param array<string, mixed> $eligible_roles
	 * @return array<string, string>
	 */
	private function sanitizeEligibleRoles( array $eligible_roles ): array {
		$role_names = wp_roles()->get_names();
		$sanitized  = [];

		foreach ( $eligible_roles as $role_key => $label ) {
			$key = sanitize_key( (string) $role_key );
			if ( $key === '' || ! isset( $role_names[ $key ] ) ) {
				continue;
			}
			$sanitized[ $key ] = is_string( $label ) ? sanitize_text_field( $label ) : (string) $role_names[ $key ];
		}

		return $sanitized;
	}

	/**
	 * @param array<int, array<string, mixed>> $config
	 */
	private function countMenuConfigItems( array $config ): int {
		$count = 0;
		$this->countMenuConfigItemsRecursive( $config, $count );

		return $count;
	}

	/**
	 * @param array<int, array<string, mixed>> $items
	 */
	private function countMenuConfigItemsRecursive( array $items, int &$count ): void {
		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			++$count;
			if ( ! empty( $item['submenu'] ) && is_array( $item['submenu'] ) ) {
				$this->countMenuConfigItemsRecursive( $item['submenu'], $count );
			}
		}
	}

	private function sanitizeMenuIcon( string $icon ): string {
		if ( $icon === '' || preg_match( '/^dashicons-[a-z0-9\-]+$/', $icon ) ) {
			return $icon;
		}

		$url = sanitize_url( $icon );
		if ( $url === '' ) {
			return 'dashicons-admin-generic';
		}

		$site_host = wp_parse_url( home_url(), PHP_URL_HOST );
		$icon_host = wp_parse_url( $url, PHP_URL_HOST );
		if ( $site_host && $icon_host && strtolower( (string) $site_host ) === strtolower( (string) $icon_host ) ) {
			return $url;
		}

		return 'dashicons-admin-generic';
	}

	private function sanitizeMenuCapability( string $capability ): string {
		$capability = sanitize_key( $capability );
		if ( $capability === '' ) {
			return 'read';
		}

		static $allowed_capabilities = null;
		if ( $allowed_capabilities === null ) {
			$allowed_capabilities = [];
			foreach ( wp_roles()->roles as $role ) {
				if ( empty( $role['capabilities'] ) || ! is_array( $role['capabilities'] ) ) {
					continue;
				}
				foreach ( array_keys( $role['capabilities'] ) as $cap ) {
					$allowed_capabilities[ sanitize_key( $cap ) ] = true;
				}
			}
		}

		return isset( $allowed_capabilities[ $capability ] ) ? $capability : 'read';
	}

	private function getAssetUrl( string $filename ): string {
		$plugin_url = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$base_url = $plugin_url . 'modules/' . self::MODULE_ID . '/assets/';
		return $base_url . $filename;
	}

	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}

		$this->settings_cache = [
			'menu_config' => $this->loadMenuConfig(),
		];

		return $this->settings_cache;
	}

	/**
	 * Load menu config from its dedicated option (autoload=no), not the shared OptionStore blob.
	 *
	 * @return array<int, array<string, mixed>>|null
	 */
	private function loadMenuConfig(): ?array {
		$this->maybeMigrateMenuConfigFromOptionStore();

		$config = get_option( self::MENU_CONFIG_OPTION, null );
		if ( is_array( $config ) && $config !== [] ) {
			return $config;
		}

		return null;
	}

	/**
	 * @param array<int, array<string, mixed>> $menu_config
	 */
	private function saveMenuConfig( array $menu_config ): bool {
		$this->settings_cache = null;

		return update_option( self::MENU_CONFIG_OPTION, $menu_config, false );
	}

	private function deleteMenuConfig(): bool {
		$this->settings_cache = null;

		return delete_option( self::MENU_CONFIG_OPTION );
	}

	/**
	 * Move legacy menu_config out of wp_powersuite_settings so it is not loaded on every request.
	 */
	private function maybeMigrateMenuConfigFromOptionStore(): void {
		if ( get_option( self::MENU_CONFIG_MIGRATED_FLAG, '' ) === '1' ) {
			return;
		}

		$existing = get_option( self::MENU_CONFIG_OPTION, null );
		if ( is_array( $existing ) && $existing !== [] ) {
			update_option( self::MENU_CONFIG_MIGRATED_FLAG, '1', false );
			return;
		}

		$settings = $this->moduleSettings()->all( [] );
		$legacy   = is_array( $settings ) ? ( $settings['menu_config'] ?? null ) : null;
		if ( is_array( $legacy ) && $legacy !== [] ) {
			update_option( self::MENU_CONFIG_OPTION, $legacy, false );
			unset( $settings['menu_config'] );
			$this->moduleSettings()->save( $settings );
		}

		update_option( self::MENU_CONFIG_MIGRATED_FLAG, '1', false );
	}

	/**
	 * Log handler failures only when WP_DEBUG is enabled.
	 */
	private function logHandlerException( string $context, \Exception $e ): void {
		if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
			return;
		}

		error_log( $context . ': ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
	}
}
