<?php
/**
 * Admin menu.
 *
 * @package suremembers
 *
 * @since 1.0.0
 */

namespace SureMembers\Admin;

use SureMembers\Inc\Access_Groups;
use SureMembers\Inc\Restricted;
use SureMembers\Inc\Traits\Get_Instance;
use SureMembers\Inc\Utils;
use SureMembers\Inc\Utils\Global_Settings;

defined( 'ABSPATH' ) || exit;

/**
 * Admin menu
 *
 * @since 0.0.1
 */
class Admin_Menu {
	use Get_Instance;

	/**
	 * Tailwind assets base url
	 *
	 * @since  0.0.1
	 * @var string
	 */
	private $tailwind_assets = SUREMEMBERS_URL . 'assets/build/';

	/**
	 * Constructor
	 *
	 * @since  0.0.1
	 */
	public function __construct() {
		add_action( 'admin_menu', [ $this, 'suremembers_access_group_page' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'settings_page_scripts' ] );
		add_action( 'admin_init', [ $this, 'check_status_transition' ] );
		add_action( 'admin_init', [ $this, 'redirect_old_urls' ] );
		add_action( 'admin_head', [ $this, 'admin_menu_css' ] );

		// Filters.
		add_filter( 'get_edit_post_link', [ $this, 'edit_post_link' ], 10, 2 );
		add_filter( 'admin_url', [ $this, 'update_add_new_link' ], 100, 2 );
		add_filter( 'suremembers_get_access_groups_data', [ $this, 'get_access_group_data' ] );
		add_filter( 'suremembers_filter_access_group_url_args', [ $this, 'check_iframe_mode' ] );
		add_filter( 'suremembers_get_access_groups_data', [ $this, 'inject_table_data' ] );
	}

	/**
	 * Redirect old URLs to new URL structure
	 *
	 * @since 1.0.0
	 */
	public function redirect_old_urls(): void {
		// Check if we're on an old URL structure.
		$current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$post_type    = isset( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended

		// Redirect old suremembers_settings to new suremembers-settings.
		if ( $current_page === 'suremembers_settings' ) {
			$new_url = admin_url( 'admin.php?page=suremembers-settings' );

			// Preserve tab parameter if exists.
			if ( isset( $_GET['tab'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$tab     = sanitize_text_field( wp_unslash( $_GET['tab'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$new_url = add_query_arg( 'tab', $tab, $new_url );
			}

			wp_safe_redirect( $new_url );
			exit;
		}

		// Redirect old suremembers_rules to new suremembers.
		if ( $current_page === 'suremembers_rules' && $post_type === SUREMEMBERS_POST_TYPE ) {
			$new_url = admin_url( 'admin.php?page=suremembers' );

			// Preserve post_id parameter if exists (convert to membership-id).
			if ( isset( $_GET['post_id'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$post_id = absint( $_GET['post_id'] ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$new_url = add_query_arg( 'membership-id', $post_id, $new_url );
			}

			wp_safe_redirect( $new_url );
			exit;
		}
	}

	/**
	 * Updates edit post link to react app
	 *
	 * @param string $url default edit url.
	 * @param int    $post_id current post id.
	 *
	 * @since 1.0.0
	 */
	public function edit_post_link( $url, $post_id ): string {
		// Ignored nonce verification as we are getting post_type from URL.
		if ( empty( $_GET['post_type'] ) || sanitize_text_field( $_GET['post_type'] ) !== SUREMEMBERS_POST_TYPE ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return $url;
		}

		$url_args = apply_filters(
			'suremembers_filter_access_group_url_args',
			[
				'page'          => 'suremembers',
				'membership-id' => $post_id,
			]
		);

		$url = add_query_arg(
			$url_args,
			admin_url( 'admin.php' )
		);
		return $url;
	}

	/**
	 * Check if edit link is loaded from iframe.
	 *
	 * @param array<string, mixed> $args URL arguments.
	 *
	 * @return array<string, mixed> $args Updated URL arguments.
	 *
	 * @since 1.0.0
	 */
	public function check_iframe_mode( $args ): array {
		// Ignoring this as we are getting the data from URL and using further.
		if ( isset( $_GET['suremembers_view'] ) && $_GET['suremembers_view'] === 'iframe' ) {// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$args['suremembers_view'] = 'iframe';
		}
		return $args;
	}

	/**
	 * Replacing link for add new Access Group
	 *
	 * @param string $url default url.
	 * @param string $path default path.
	 *
	 * @since 1.0.0
	 */
	public function update_add_new_link( $url, $path ): string {
		if ( $path === 'post-new.php?post_type=' . SUREMEMBERS_POST_TYPE ) {
			$url = Access_Groups::get_admin_url( [ 'membership-id' => 'new' ] );
		}
		return $url;
	}

	/**
	 * Adds admin menu for settings page
	 *
	 * @since  2.0.0
	 */
	public function suremembers_access_group_page(): void {
		// Base64 encoded SVG icon for WordPress admin menu (like SureDash).
		$logo = 'data:image/svg+xml;base64, ';

		// Register main memberships page as standalone admin page (like SureDash).
		add_menu_page(
			__( 'SureMembers', 'suremembers' ),
			__( 'SureMembers', 'suremembers' ),
			'manage_options',
			'suremembers',
			[ $this, 'render' ],
			'data:image/svg+xml;base64,' . $logo, //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
			30
		);

		// Add submenu for memberships (will replace the auto-generated first item).
		add_submenu_page(
			'suremembers',
			__( 'Dashboard', 'suremembers' ),
			__( 'Dashboard', 'suremembers' ),
			'manage_options',
			'suremembers',
			[ $this, 'render' ]
		);

		add_submenu_page(
			'suremembers',
			__( 'Memberships', 'suremembers' ),
			__( 'Memberships', 'suremembers' ),
			'manage_options',
			'suremembers&tab=memberships',
			[ $this, 'render' ]
		);

			// Add users submenu.
		add_submenu_page(
			'suremembers',
			__( 'Users', 'suremembers' ),
			__( 'Users', 'suremembers' ),
			'manage_options',
			'suremembers&tab=users',
			[ $this, 'render' ]
		);

		// Add settings submenu.
		add_submenu_page(
			'suremembers',
			__( 'Settings', 'suremembers' ),
			__( 'Settings', 'suremembers' ),
			'manage_options',
			'suremembers&tab=settings',
			[ $this, 'render' ]
		);

		// Add settings submenu.
		add_submenu_page(
			'suremembers',
			__( 'SureDash', 'suremembers' ),
			__( 'SureDash', 'suremembers' ),
			'manage_options',
			'suremembers&tab=suredash',
			[ $this, 'render' ]
		);

		// Add hidden onboarding page (similar to SureDash).
		add_submenu_page(
			'suremembers',
			'SureMembers ' . __( 'Onboarding', 'suremembers' ),
			'Onboarding',
			'manage_options',
			'suremembers-onboarding',
			[ $this, 'render_onboarding' ]
		);

		// Hide the old CPT menu completely.
		global $submenu;
		global $menu;

		// Remove all submenus under the CPT parent.
		$parent_slug = 'edit.php?post_type=' . SUREMEMBERS_POST_TYPE;

		if ( isset( $submenu[ $parent_slug ] ) ) {
			unset( $submenu[ $parent_slug ] );
		}

		// Remove the main CPT menu item from sidebar.
		foreach ( $menu as $key => $item ) {
			if ( isset( $item[2] ) && $parent_slug === $item[2] ) {
				unset( $menu[ $key ] );
				break;
			}
		}
	}

	/**
	 * Render the admin page.
	 *
	 * @return void
	 */
	public function render(): void {
		// Use same container for both pages (unified app approach like SureDash).
		?>
		<div class="suremembers-access-group" id="suremembers-access-group"></div>
		<?php
	}

	/**
	 * Render onboarding page
	 *
	 * @since 1.0.0
	 */
	public function render_onboarding(): void {
		?>
		<div class="suremembers-onboarding" id="suremembers-onboarding"></div>
		<?php
	}

	/**
	 * Enqueue settings page script and style
	 *
	 * @param string $hook current page hook.
	 *
	 * @since  1.0.0
	 */
	public function settings_page_scripts( $hook ): void {
		// Check if we're on SureMembers pages.
		$current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		// Ignoring this as we are getting the data from URL and using further.
		if ( isset( $_GET['suremembers_view'] ) && $_GET['suremembers_view'] === 'iframe' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			wp_enqueue_style( 'suremembers-admin-frame-popup', SUREMEMBERS_URL . 'admin/assets/build/adminbarpopup.css', [], SUREMEMBERS_VER );

			// Add inline JavaScript for iframe mode.
			wp_add_inline_script(
				'jquery',
				'
				jQuery(document).ready(function($) {
					// Hide WordPress admin elements immediately
					$("#wpadminbar, #adminmenumain, #adminmenuwrap, #adminmenu, #adminmenu-back").hide();
					$("#wpcontent").css({"margin-left": "0", "padding-left": "0", "width": "100%"});
					$("#wpbody").css({"margin-left": "0", "padding-left": "0"});
					$("#wpfooter").hide();

					// Handle compact mode
					if (window.location.search.includes("suremembers_compact=1")) {
						// Add compact data attribute for CSS targeting
						$("body").attr("data-compact", "1");

						// Remove unnecessary WordPress elements for faster loading
						$(".notice, .error, .updated").hide();
						$("#wpbody-content").css({"margin-left": "0", "padding-left": "0"});
						$(".wrap").css({"margin": "0", "padding": "20px"});

						// Optimize for faster loading
						$("script[src*=\'admin-bar\']").remove();
						$("script[src*=\'common\']").remove();
						$("script[src*=\'heartbeat\']").remove();
						$("script[src*=\'dashboard\']").remove();
						$("script[src*=\'postbox\']").remove();

						// Add loading optimization
						$("img").each(function() {
							if ($(this).attr("src") && $(this).attr("src").includes("loading")) {
								$(this).attr("loading", "lazy");
							}
						});

						// Disable WordPress heartbeat for faster performance
						if (typeof wp !== "undefined" && wp.heartbeat) {
							wp.heartbeat.interval(0);
						}
					}
				});
			'
			);
		}

		$screen = get_current_screen();

		if ( ! is_null( $screen ) && $screen->id === 'edit-' . SUREMEMBERS_POST_TYPE ) {
			wp_enqueue_style( 'suremembers-license-popup', SUREMEMBERS_URL . 'admin/assets/css/license-popup.css', [], SUREMEMBERS_VER );
			wp_enqueue_script( 'suremembers-license-popup-js', SUREMEMBERS_URL . 'admin/assets/js/license-popup.js', [], SUREMEMBERS_VER, true );
		}

		// Check if we're on one of the SureMembers admin pages.
		$suremembers_pages = [
			'toplevel_page_suremembers',
			'suremembers_page_suremembers-settings',
			'suremembers_page_suremembers-settings', // Fallback for different WordPress versions.
		];

		if (
			is_null( $screen ) ||
			( ! in_array( $screen->id, $suremembers_pages, true ) && $current_page !== 'suremembers-onboarding' )
		) {
			return;
		}

		$page       = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$post_count = 0; // Initialize with default value.

		// Use unified suremembers app for all pages.
		$app = 'suremembers';

		// Get post count for memberships page.
		if ( in_array( $page, [ 'suremembers', 'suremembers-settings' ], true ) ) {
			$post_count = wp_count_posts( SUREMEMBERS_POST_TYPE )->publish;
		}

		$script_asset_path = SUREMEMBERS_DIR . 'assets/build/' . $app . '.asset.php';
		$script_info       = file_exists( $script_asset_path )
			? include $script_asset_path
			: [
				'dependencies' => [],
				'version'      => SUREMEMBERS_VER,
			];

		$external_deps = [ 'updates' ];

		/**
		 * Check for modules dependencies.
		 * `suremembers-modules` is the handle of the script JS generated when
		 * external modules are loaded.
		 */
		if ( wp_script_is( 'suremembers-modules', 'registered' ) ) {
			array_push( $external_deps, 'suremembers-modules' );
		}

		$script_dep = array_merge( $script_info['dependencies'], $external_deps );

		// Enqueue media library for file uploads.
		wp_enqueue_media();

		wp_register_script( 'suremembers_posts', $this->tailwind_assets . $app . '.js', $script_dep, SUREMEMBERS_VER, true );
		wp_enqueue_script( 'suremembers_posts' );
		$suremembers_post_types = Restricted::get_post_types( 'object' );
		$locations              = $this->get_location_selections();
		$drips                  = $this->get_drip_data();

		$list_url_args = [];
		// Ignoring this as we are getting the data from URL and using further.
		if ( isset( $_GET['suremembers_view'] ) && $_GET['suremembers_view'] === 'iframe' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$list_url_args['suremembers_view'] = 'iframe';
		}

		$list_archive_url = Access_Groups::get_admin_url( $list_url_args );

		$localize_data = [
			'ajax_url'               => admin_url( 'admin-ajax.php' ),
			'post_url'               => admin_url( 'admin.php?page=suremembers' ),
			'add_new_url'            => admin_url( 'admin.php?page=suremembers' ),
			'posts_nonce'            => current_user_can( 'manage_options' ) ? wp_create_nonce( 'suremembers_posts_nonce' ) : '',
			'submit_nonce'           => current_user_can( 'manage_options' ) ? wp_create_nonce( 'suremembers_submit_nonce' ) : '',
			'search_nonce'           => current_user_can( 'edit_posts' ) ? wp_create_nonce( 'suremembers_search_post_nonce' ) : '',
			'uploads_nonce'          => current_user_can( 'edit_posts' ) ? wp_create_nonce( 'suremembers_uploads_nonce' ) : '',
			'status_nonce'           => wp_create_nonce( 'suremembers_status_nonce' ),
			'list_url'               => $list_archive_url,
			'post_types'             => array_combine( array_keys( $suremembers_post_types ), array_column( array_column( $suremembers_post_types, 'labels' ), 'name' ) ),
			'locations'              => $locations,
			'selected_locations'     => $this->get_selected_locations(),
			'specific_locations'     => $this->get_individual_data( 'specific' ),
			'exclude_locations'      => $this->get_individual_data(),
			'drip_data'              => $drips,
			'restricted_url'         => $this->get_restricted_url_data(),
			'user_roles_choises'     => $this->get_user_roles_with_custom_roles(),
			'user_roles_selected'    => Access_Groups::get_selected_user_roles(),
			'access_group_downloads' => Access_Groups::get_downloads(),
			'access_group_count'     => $post_count,
		];

		wp_localize_script(
			'suremembers_posts',
			'suremembers_posts',
			apply_filters( 'suremembers_get_access_groups_data', $localize_data )
		);

		// Add global settings data available across all SureMembers pages.
		// Note: Settings are now fetched via REST API on demand (following SureDash pattern).
		// Only essential non-settings data is passed here.

		$settings_data = [
			'version'                   => SUREMEMBERS_VER,
			'post_type'                 => SUREMEMBERS_POST_TYPE,
			'user_roles'                => $this->get_user_roles_with_custom_roles(),
			'ajax_nonce'                => current_user_can( 'manage_options' ) ? wp_create_nonce( 'suremembers_global_settings_nonce' ) : '',
			'redirect_rules'            => $this->get_redirection_rules(),
			'registration_access_group' => $this->get_selected_registration_access_group(),
			'custom_content'            => Global_Settings::get_custom_content_data(),
			'woocommerce_active'        => function_exists( 'WC' ),
			'home_url'                  => home_url( '/' ),
			'license_status'            => Utils::is_license_activated(),
			'username'                  => wp_get_current_user()->display_name,
			'sureforms_status'          => $this->get_plugin_status( 'sureforms/sureforms.php' ),
			'surecart_status'           => $this->get_plugin_status( 'surecart/surecart.php' ),
			'ottokit_status'            => $this->get_plugin_status( 'suretriggers/suretriggers.php' ),
			'suredash_status'           => $this->get_plugin_status( 'suredash/suredash.php' ),
			'suremails_status'          => $this->get_plugin_status( 'suremails/suremails.php' ),
			'suredash_icon_url'         => SUREMEMBERS_URL . 'assets/images/suredash-icon.svg',
			'wp_rest_nonce'             => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : '',
		];

		wp_localize_script(
			'suremembers_posts',
			'suremembers_data',
			$settings_data
		);

		// Add onboarding-specific localization if we're on the onboarding page.
		$current_user = wp_get_current_user();
			wp_localize_script(
				'suremembers_posts',
				'suremembers_onboarding_data',
				[
					'ajax_url'                   => admin_url( 'admin-ajax.php' ),
					'nonce'                      => wp_create_nonce( 'suremembers_onboarding_nonce' ),
					'dashboard_url'              => admin_url( 'admin.php?page=suremembers' ),
					'suredash_onboarding_image'  => SUREMEMBERS_URL . 'assets/images/suredash-onboarding-cropped.svg',
					'surecart_onboarding_image'  => SUREMEMBERS_URL . 'assets/images/surecart-onboarding.svg',
					'suremails_onboarding_image' => SUREMEMBERS_URL . 'assets/images/suremails-onboarding.png',
					'current_user'               => [
						'first_name' => $current_user->first_name,
						'last_name'  => $current_user->last_name,
						'email'      => $current_user->user_email,
					],
				]
			);
			// Add admin settings for onboarding page.
			wp_localize_script(
				'suremembers_posts',
				'suremembers_settings',
				[
					'admin_settings' => \SureMembers\Inc\Settings::get_setting( 'suremembers_admin_settings' ),
				]
			);

		wp_register_style( 'suremembers_posts', $this->tailwind_assets . $app . '.css', [ 'wp-components' ], SUREMEMBERS_VER );
		wp_enqueue_style( 'suremembers_posts' );

		wp_set_script_translations( 'suremembers_posts', 'suremembers', SUREMEMBERS_DIR . 'languages' );
	}

	/**
	 * Get user roles array also available in settings-screen.php.
	 *
	 * @return array<string, mixed> array of user roles.
	 *
	 * @since 1.1.0
	 */
	public function get_user_roles_with_custom_roles(): array {
		global $wp_roles;

		if ( ! isset( $wp_roles ) ) {
			return [];
		}

		$available_roles_names = $wp_roles->get_names();
		$excluded_roles        = apply_filters( 'suremembers_settings_excluded_roles', [ 'administrator' => esc_html__( 'Administrator', 'suremembers' ) ] );
		$included_roles        = array_diff( $available_roles_names, $excluded_roles );
		return Utils::get_react_select_format( $included_roles );
	}

	/**
	 * Add the CSS to design the main side-bar menu of the plugin.
	 *
	 * @since 1.10.0
	 */
	public function admin_menu_css(): void {

			echo '<style>
				/* Hide SureDash submenu under SureMembers */
				li a[href="admin.php?page=suremembers&tab=suredash"] {
					display: none !important;
				}
				
				/* Add separator lines between menu items (like SureDash) */
				#toplevel_page_suremembers li {
					clear: both;
				}
				#toplevel_page_suremembers li:not(:last-child) a[href^="admin.php?page=suremembers"]:after {
					border-bottom: 1px solid hsla(0,0%,100%,.2);
					display: block;
					float: left;
					margin: 13px -15px 8px;
					content: "";
					width: calc(100% + 26px);
				}
				/* Remove separator after Memberships (grouped with Settings) */
				#toplevel_page_suremembers li:not(:last-child) a[href^="admin.php?page=suremembers&tab=memberships"]:after {
					content: none;
				}
				/* Remove separator after Users (last item) */
				#toplevel_page_suremembers li:not(:last-child) a[href^="admin.php?page=suremembers&tab=settings"]:after {
					content: none;
				}
				li a[href="admin.php?page=suremembers-onboarding"] {
					display: none !important;
				}
			</style>';

		// Hide WordPress admin elements for onboarding page (similar to SureDash).
		$current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( $current_page === 'suremembers-onboarding' ) {
			// phpcs:disable WordPressVIPMinimum.UserExperience.AdminBarRemoval.HidingDetected -- Intentional for onboarding page, similar to SureDash.
			echo '<style>
				#adminmenumain,#wpadminbar,#wpfooter {
					display:none;
				}
				#wpcontent{
					margin-left:0;
					padding-left:0px;
				}
				html.wp-toolbar {
					padding-top: 0;
				}
			</style>';
			// phpcs:enable
		}
	}

	/**
	 * Access plan.
	 *
	 * @since 1.0.0
	 */
	public function get_block_restriction_access_groups(): array|bool {
		$get_access_groups = Access_Groups::get_active();
		if ( empty( $get_access_groups ) ) {
			return false;
		}
		$return = [];
		foreach ( $get_access_groups as $key => $value ) {
			$return[] = [
				'id'    => $key,
				'title' => $value,
			];
		}
		return $return;
	}

	/**
	 * Return data for edit Access Group
	 *
	 * @param array<string, mixed> $data existing data.
	 *
	 * @return array<string, mixed>
	 *
	 * @since 1.0.0
	 */
	public function get_access_group_data( $data ): array {
		// Ignored as we are using this to localize data.
		if ( empty( $_GET['post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return $data;
		}
		// Ignored as we are using this to localize data.
		$id = absint( $_GET['post_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( empty( $id ) ) {
			return $data;
		}

		$post = get_post( $id );

		if ( empty( $post ) || $post->post_type !== SUREMEMBERS_POST_TYPE ) {
			return $data;
		}

		$data['post_data']['title']      = $post->post_title;
		$data['post_id']                 = $id;
		$data['post_status']             = get_post_status( $id );
		$data['post_data']['priority']   = get_post_meta( $id, SUREMEMBERS_PLAN_PRIORITY, true );
		$data['post_data']['expiration'] = get_post_meta( $id, SUREMEMBERS_PLAN_EXPIRATION, true );

		$meta = get_post_meta( $id, SUREMEMBERS_PLAN_RULES, true );
		if ( empty( $meta ) ) {
			return $data;
		}

		$data['post_data']['meta'] = $meta;
		return $data;
	}

	/**
	 * Get data for Access Groups Table.
	 *
	 * @return array<string, mixed>
	 *
	 * @since 2.0.0
	 */
	public function get_access_groups_table_data(): array {
		$args = [
			'post_type'      => SUREMEMBERS_POST_TYPE,
			'post_status'    => [ 'publish', 'draft', 'pending', 'private', SUREMEMBERS_ARCHIVE ],
			'posts_per_page' => -1,
		];

		$query = new \WP_Query( $args );
		$data  = [];

		if ( ! $query->have_posts() ) {
			return $data;
		}

		foreach ( $query->posts as $post ) {
			$post_id = $post->ID;

			// Fetch include and exclude meta (should be arrays with 'specifics' and 'rules').
			$include = get_post_meta( $post_id, SUREMEMBERS_PLAN_INCLUDE, true );
			$exclude = get_post_meta( $post_id, SUREMEMBERS_PLAN_EXCLUDE, true );

			// Get specifics (post IDs) for include/exclude and remove duplicates.
			$include_specifics = is_array( $include ) && ! empty( $include['specifics'] ) ? array_unique( $include['specifics'] ) : [];
			$exclude_rules     = is_array( $exclude ) && ! empty( $exclude['rules'] ) ? array_unique( $exclude['rules'] ) : [];

			// Fetch post info for include specifics.
			$include_posts = [];
			$processed_ids = []; // Track processed post IDs to avoid duplicates.
			foreach ( $include_specifics as $inc_id ) {
				// Extract numeric post ID from string like 'post-199-|' or 'postchild-199-|'.
				if ( preg_match( '/(post(?:child)?)-(\d+)-\|?/', $inc_id, $matches ) ) {
					$prefix  = $matches[1];
					$real_id = intval( $matches[2] );

					// Skip if already processed.
					if ( in_array( $prefix . '-' . $real_id, $processed_ids, true ) ) {
						continue;
					}

					$inc_post = get_post( $real_id );
					if ( $inc_post ) {
						$processed_ids[] = $prefix . '-' . $real_id;
						$include_posts[] = [
							'id'    => $inc_post->ID,
							'title' => $prefix === 'postchild' ? sprintf( /* translators: %s title. */ __( 'Child of %s', 'suremembers' ), get_the_title( $inc_post ) ) : get_the_title( $inc_post ),
							'type'  => get_post_type( $inc_post ),
							'link'  => get_edit_post_link( $inc_post->ID ),
							'value' => $inc_id,
						];
					}
				} elseif ( preg_match( '/^tax-(\d+)-(single|archive)-(.+)$/', $inc_id, $matches ) ) {
					// Handle taxonomy patterns like 'tax-7-single-portal_group' or 'tax-7-archive-portal_group'.
					$term_id  = intval( $matches[1] );
					$taxonomy = $matches[3];

					// Skip if already processed.
					if ( in_array( $inc_id, $processed_ids, true ) ) {
						continue;
					}

					$term = get_term( $term_id, $taxonomy );
					if ( $term && ! is_wp_error( $term ) ) {
						$processed_ids[] = $inc_id;
						$include_posts[] = [
							'id'    => $term_id,
							/* translators: %s term name. */
							'title' => sprintf( __( 'All singulars from %s', 'suremembers' ), $term->name ),
							'type'  => $taxonomy,
							'link'  => get_edit_term_link( $term_id, $taxonomy ),
							'value' => $inc_id,
						];
					}
				}
			}

			// Fetch post info for exclude rules.
			$exclude_posts         = [];
			$processed_exclude_ids = []; // Track processed post IDs to avoid duplicates.
			foreach ( $exclude_rules as $exc_id ) {
				if ( preg_match( '/(post(?:child)?)-(\d+)-\|?/', $exc_id, $matches ) ) {
					$prefix  = $matches[1];
					$real_id = intval( $matches[2] );

					// Skip if already processed.
					if ( in_array( $prefix . '-' . $real_id, $processed_exclude_ids, true ) ) {
						continue;
					}

					$exc_post = get_post( $real_id );
					if ( $exc_post ) {
						$processed_exclude_ids[] = $prefix . '-' . $real_id;
						$exclude_posts[]         = [
							'id'    => $exc_post->ID,
							'title' => $prefix === 'postchild' ? sprintf( /* translators: %s title. */ __( 'Child of %s', 'suremembers' ), get_the_title( $exc_post ) ) : get_the_title( $exc_post ),
							'type'  => get_post_type( $exc_post ),
							'link'  => get_edit_post_link( $exc_post->ID ),
							'value' => $exc_id,
						];
					}
				} elseif ( preg_match( '/^tax-(\d+)-(single|archive)-(.+)$/', $exc_id, $matches ) ) {
					// Handle taxonomy patterns like 'tax-7-single-portal_group' or 'tax-7-archive-portal_group'.
					$term_id  = intval( $matches[1] );
					$taxonomy = $matches[3];

					// Skip if already processed.
					if ( in_array( $exc_id, $processed_exclude_ids, true ) ) {
						continue;
					}

					$term = get_term( $term_id, $taxonomy );
					if ( $term && ! is_wp_error( $term ) ) {
						$processed_exclude_ids[] = $exc_id;
						$exclude_posts[]         = [
							'id'    => $term_id,
							/* translators: %s term name. */
							'title' => sprintf( __( 'All singulars from %s', 'suremembers' ), $term->name ),
							'type'  => $taxonomy,
							'link'  => get_edit_term_link( $term_id, $taxonomy ),
							'value' => $exc_id,
						];
					}
				}
			}

			// Get user count (matches admin table logic).
			$required_fetch = get_post_meta( $post_id, SUREMEMBERS_REQUIRES_QUERY, true );
			if ( ! empty( $required_fetch ) ) {
				$users_in_access_group = \SureMembers\Inc\Access_Groups::get_users_count( $post_id );
			} else {
				$users_in_access_group = absint( get_post_meta( $post_id, SUREMEMBERS_PLAN_ACTIVE_USERS, true ) );
			}

			$data[] = [
				'id'              => $post_id,
				'title'           => $post->post_title,
				'status'          => get_post_status( $post_id ),
				'priority'        => get_post_meta( $post_id, SUREMEMBERS_PLAN_PRIORITY, true ),
				'expiration'      => get_post_meta( $post_id, SUREMEMBERS_PLAN_EXPIRATION, true ),
				'date'            => get_the_date( 'c', $post ),
				'rules'           => get_post_meta( $post_id, SUREMEMBERS_PLAN_RULES, true ),
				'edit_link'       => \SureMembers\Inc\Access_Groups::get_admin_url(
					[
						'membership-id' => $post_id,
					]
				),
				'include'         => $include,
				'exclude'         => $exclude, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Array key for data structure, not WP_Query parameter.
				'include_posts'   => $include_posts,
				'exclude_posts'   => $exclude_posts,
				'user_count'      => $users_in_access_group,
				'restricted_url'  => get_post_meta( $post_id, SUREMEMBERS_RESTRICTED_URL, true ),
				'user_roles'      => get_post_meta( $post_id, SUREMEMBERS_USER_ROLES, true ),
				'drips'           => get_post_meta( $post_id, SUREMEMBERS_PLAN_DRIPS, true ),
				'downloads'       => get_post_meta( $post_id, SUREMEMBERS_ACCESS_GROUP_DOWNLOADS, true ),
				'meta'            => [
					'restrict' => get_post_meta( $post_id, SUREMEMBERS_PLAN_INCLUDE, true ) ? get_post_meta( $post_id, SUREMEMBERS_PLAN_INCLUDE, true ) : [
						'preview_button'      => '',
						'redirect_url'        => '',
						'excerpt'             => false,
						'in_content'          => false,
						'enablelogin'         => false,
						'preview_content'     => '',
						'unauthorized_action' => '',
					],
				],
				'user_filter_url' => add_query_arg(
					[
						'suremembers_access_group_top'    => $post_id,
						'suremembers_access_group_bottom' => $post_id,
					],
					admin_url( 'users.php' )
				),
			];
		}

		wp_reset_postdata();
		return $data;
	}

	/**
	 * Injects Access Groups table data into localized script.
	 *
	 * @param array<string, mixed> $data The existing data passed to the JS.
	 */
	public function inject_table_data( $data ): array {
		$data['table_data'] = $this->get_access_groups_table_data();
		return $data;
	}

	/**
	 * Get table data for refreshing the access groups list (returns data instead of echoing JSON).
	 *
	 * @return array{success: bool, data?: array<string, mixed>, message?: string} Result array.
	 *
	 * @since 1.10.14
	 */
	public function get_table_data_result(): array {
		if ( ! current_user_can( 'manage_options' ) ) {
			return [
				'success' => false,
				'message' => __( 'Insufficient permissions.', 'suremembers' ),
			];
		}

		$table_data = $this->get_access_groups_table_data();

		return [
			'success' => true,
			'data'    => [ 'table_data' => $table_data ],
		];
	}

	/**
	 * AJAX handler to get table data for refreshing the access groups list (uses get_table_data_result).
	 *
	 * @since 2.0.0
	 */
	public function get_table_data_ajax(): void {
		$result = $this->get_table_data_result();

		if ( $result['success'] ) {
			wp_send_json_success( $result['data'] ?? [] );
		} else {
			wp_send_json_error( [ 'message' => $result['message'] ?? '' ] );
		}
	}

	/**
	 * Get post data for editing access groups (returns data instead of echoing JSON).
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return array{success: bool, data?: array<string, mixed>, message?: string} Result array.
	 *
	 * @since 1.10.14
	 */
	public function get_post_data_result( $post_id ): array {
		try {
			if ( ! current_user_can( 'manage_options' ) ) {
				return [
					'success' => false,
					'message' => __( 'Insufficient permissions.', 'suremembers' ),
				];
			}

			if ( empty( $post_id ) ) {
				return [
					'success' => false,
					'message' => __( 'Post ID is required.', 'suremembers' ),
				];
			}

			$post = get_post( $post_id );
			if ( ! $post || $post->post_type !== SUREMEMBERS_POST_TYPE ) {
				return [
					'success' => false,
					'message' => __( 'Post not found.', 'suremembers' ),
				];
			}

			// Get all post meta data.
			$include_data        = get_post_meta( $post->ID, SUREMEMBERS_PLAN_INCLUDE, true );
			$rules_data          = get_post_meta( $post->ID, SUREMEMBERS_PLAN_RULES, true );
			$exclude_data        = get_post_meta( $post->ID, SUREMEMBERS_PLAN_EXCLUDE, true );
			$restricted_url_data = get_post_meta( $post->ID, SUREMEMBERS_RESTRICTED_URL, true );
			$drips_data          = get_post_meta( $post->ID, SUREMEMBERS_PLAN_DRIPS, true );
			$expiration_data     = get_post_meta( $post->ID, SUREMEMBERS_PLAN_EXPIRATION, true );
			$priority_data       = get_post_meta( $post->ID, SUREMEMBERS_PLAN_PRIORITY, true );
			$user_roles_data     = get_post_meta( $post->ID, SUREMEMBERS_USER_ROLES, true );
			$downloads_data      = get_post_meta( $post->ID, SUREMEMBERS_ACCESS_GROUP_DOWNLOADS, true );

			// Format specific locations for SpecificSelectField.
			$specific_locations = [];
			if ( ! empty( $include_data['specifics'] ) && is_array( $include_data['specifics'] ) ) {
				foreach ( $include_data['specifics'] as $specific_id ) {
					$specific_post = get_post( $specific_id );
					if ( $specific_post ) {
						$specific_locations[] = [
							'value' => $specific_id,
							'label' => $specific_post->post_title . ' (' . $specific_post->post_type . ')',
						];
					}
				}
			}

			// Get specifics (post IDs) for include/exclude and remove duplicates.
			$include_specifics = is_array( $include_data ) && ! empty( $include_data['specifics'] ) ? array_unique( $include_data['specifics'] ) : [];
			$exclude_rules     = is_array( $exclude_data ) && ! empty( $exclude_data['rules'] ) ? array_unique( $exclude_data['rules'] ) : [];

			// Fetch post info for include specifics.
			$include_posts = [];
			$processed_ids = []; // Track processed post IDs to avoid duplicates.
			foreach ( $include_specifics as $inc_id ) {
				// Extract numeric post ID from string like 'post-199-|' or 'postchild-199-|'.
				if ( preg_match( '/(post(?:child)?)-(\d+)-\|?/', $inc_id, $matches ) ) {
					$prefix  = $matches[1];
					$real_id = intval( $matches[2] );

					// Skip if already processed.
					if ( in_array( $prefix . '-' . $real_id, $processed_ids, true ) ) {
						continue;
					}

					$inc_post = get_post( $real_id );
					if ( $inc_post ) {
						$processed_ids[] = $prefix . '-' . $real_id;
						$include_posts[] = [
							'id'    => $inc_post->ID,
							'title' => $prefix === 'postchild' ? sprintf( /* translators: %s title. */ __( 'Child of %s', 'suremembers' ), get_the_title( $inc_post ) ) : get_the_title( $inc_post ),
							'type'  => get_post_type( $inc_post ),
							'link'  => get_edit_post_link( $inc_post->ID ),
							'value' => $inc_id,
						];
					}
				} elseif ( preg_match( '/^tax-(\d+)-(single|archive)-(.+)$/', $inc_id, $matches ) ) {
					// Handle taxonomy patterns like 'tax-7-single-portal_group' or 'tax-7-archive-portal_group'.
					$term_id  = intval( $matches[1] );
					$taxonomy = $matches[3];

					// Skip if already processed.
					if ( in_array( $inc_id, $processed_ids, true ) ) {
						continue;
					}

					$term = get_term( $term_id, $taxonomy );
					if ( $term && ! is_wp_error( $term ) ) {
						$processed_ids[] = $inc_id;
						$include_posts[] = [
							'id'    => $term_id,
							/* translators: %s term name. */
							'title' => sprintf( __( 'All singulars from %s', 'suremembers' ), $term->name ),
							'type'  => $taxonomy,
							'link'  => get_edit_term_link( $term_id, $taxonomy ),
							'value' => $inc_id,
						];
					}
				}
			}

			// Fetch post info for exclude rules.
			$exclude_posts         = [];
			$processed_exclude_ids = []; // Track processed post IDs to avoid duplicates.
			foreach ( $exclude_rules as $exc_id ) {
				if ( preg_match( '/(post(?:child)?)-(\d+)-\|?/', $exc_id, $matches ) ) {
					$prefix  = $matches[1];
					$real_id = intval( $matches[2] );

					// Skip if already processed.
					if ( in_array( $prefix . '-' . $real_id, $processed_exclude_ids, true ) ) {
						continue;
					}

					$exc_post = get_post( $real_id );
					if ( $exc_post ) {
						$processed_exclude_ids[] = $prefix . '-' . $real_id;
						$exclude_posts[]         = [
							'id'    => $exc_post->ID,
							'title' => $prefix === 'postchild' ? sprintf( /* translators: %s title. */ __( 'Child of %s', 'suremembers' ), get_the_title( $exc_post ) ) : get_the_title( $exc_post ),
							'type'  => get_post_type( $exc_post ),
							'link'  => get_edit_post_link( $exc_post->ID ),
							'value' => $exc_id,
						];
					}
				} elseif ( preg_match( '/^tax-(\d+)-(single|archive)-(.+)$/', $exc_id, $matches ) ) {
					// Handle taxonomy patterns like 'tax-7-single-portal_group' or 'tax-7-archive-portal_group'.
					$term_id  = intval( $matches[1] );
					$taxonomy = $matches[3];

					// Skip if already processed.
					if ( in_array( $exc_id, $processed_exclude_ids, true ) ) {
						continue;
					}

					$term = get_term( $term_id, $taxonomy );
					if ( $term && ! is_wp_error( $term ) ) {
						$processed_exclude_ids[] = $exc_id;
						$exclude_posts[]         = [
							'id'    => $term_id,
							/* translators: %s term name. */
							'title' => sprintf( __( 'All singulars from %s', 'suremembers' ), $term->name ),
							'type'  => $taxonomy,
							'link'  => get_edit_term_link( $term_id, $taxonomy ),
							'value' => $exc_id,
						];
					}
				}
			}

			// Process drips to fetch post titles for rules (same as include_posts/exclude_posts).
			$processed_drips = [];
			if ( ! empty( $drips_data ) && is_array( $drips_data ) ) {
				foreach ( $drips_data as $drip ) {
					$drip_rules         = [];
					$processed_drip_ids = [];
					if ( ! empty( $drip['rules'] ) && is_array( $drip['rules'] ) ) {
						foreach ( $drip['rules'] as $rule_id ) {
							if ( preg_match( '/(post(?:child)?)-(\d+)-\|?/', $rule_id, $matches ) ) {
								$prefix  = $matches[1];
								$real_id = intval( $matches[2] );

								// Skip if already processed.
								if ( in_array( $prefix . '-' . $real_id, $processed_drip_ids, true ) ) {
									continue;
								}

								$rule_post = get_post( $real_id );
								if ( $rule_post ) {
									$processed_drip_ids[] = $prefix . '-' . $real_id;
									$drip_rules[]         = [
										'id'    => $rule_post->ID,
										'title' => $prefix === 'postchild' ? sprintf( /* translators: %s title. */ __( 'Child of %s', 'suremembers' ), get_the_title( $rule_post ) ) : get_the_title( $rule_post ),
										'type'  => get_post_type( $rule_post ),
										'link'  => get_edit_post_link( $rule_post->ID ),
										'value' => $rule_id,
									];
								}
							} elseif ( preg_match( '/^tax-(\d+)-(single|archive)-(.+)$/', $rule_id, $matches ) ) {
								// Handle taxonomy patterns like 'tax-7-single-portal_group'.
								$term_id  = intval( $matches[1] );
								$taxonomy = $matches[3];

								// Skip if already processed.
								if ( in_array( $rule_id, $processed_drip_ids, true ) ) {
									continue;
								}

								$term = get_term( $term_id, $taxonomy );
								if ( $term && ! is_wp_error( $term ) ) {
									$processed_drip_ids[] = $rule_id;
									$drip_rules[]         = [
										'id'    => $term_id,
										/* translators: %s term name. */
										'title' => sprintf( __( 'All singulars from %s', 'suremembers' ), $term->name ),
										'type'  => $taxonomy,
										'link'  => get_edit_term_link( $term_id, $taxonomy ),
										'value' => $rule_id,
									];
								}
							}
						}
					}
					$processed_drips[] = array_merge( $drip, [ 'rules' => $drip_rules ] );
				}
			}

			// Process LearnDash courses data to include post titles.
			$learndash_courses      = [];
			$learndash_courses_data = is_array( $include_data ) && ! empty( $include_data['learndash_courses'] ) ? $include_data['learndash_courses'] : [];
			foreach ( $learndash_courses_data as $ld_course ) {
				// Handle 'post-123-|' format.
				if ( preg_match( '/(post(?:child)?)-(\d+)-\|?/', $ld_course, $matches ) ) {
					$prefix   = $matches[1];
					$real_id  = intval( $matches[2] );
					$post_obj = get_post( $real_id );
					if ( $post_obj ) {
						$learndash_courses[] = [
							'id'    => $post_obj->ID,
							'title' => $prefix === 'postchild' ? sprintf( /* translators: %s title. */ __( 'Child of %s', 'suremembers' ), get_the_title( $post_obj ) ) : get_the_title( $post_obj ),
							'type'  => get_post_type( $post_obj ),
							'link'  => get_edit_post_link( $post_obj->ID ),
							'value' => $ld_course,
						];
					}
				} elseif ( strpos( $ld_course, '|all' ) !== false ) {
					// Handle 'sfwd-courses|all' format.
					$post_type = str_replace( '|all', '', $ld_course );
					if ( post_type_exists( $post_type ) ) {
						$post_type_obj       = get_post_type_object( $post_type );
						$learndash_courses[] = [
							'value' => $ld_course,
							/* translators: %s post type label. */
							'label' => sprintf( __( 'All %s', 'suremembers' ), $post_type_obj->labels->name ?? $post_type ),
						];
					}
				}
			}

			// Process LearnDash groups data to include post titles.
			$learndash_groups      = [];
			$learndash_groups_data = is_array( $include_data ) && ! empty( $include_data['learndash_groups'] ) ? $include_data['learndash_groups'] : [];
			foreach ( $learndash_groups_data as $ld_group ) {
				// Handle 'post-123-|' format.
				if ( preg_match( '/(post(?:child)?)-(\d+)-\|?/', $ld_group, $matches ) ) {
					$prefix   = $matches[1];
					$real_id  = intval( $matches[2] );
					$post_obj = get_post( $real_id );
					if ( $post_obj ) {
						$learndash_groups[] = [
							'id'    => $post_obj->ID,
							'title' => $prefix === 'postchild' ? sprintf( /* translators: %s title. */ __( 'Child of %s', 'suremembers' ), get_the_title( $post_obj ) ) : get_the_title( $post_obj ),
							'type'  => get_post_type( $post_obj ),
							'link'  => get_edit_post_link( $post_obj->ID ),
							'value' => $ld_group,
						];
					}
				} elseif ( strpos( $ld_group, '|all' ) !== false ) {
					// Handle 'groups|all' format.
					$post_type = str_replace( '|all', '', $ld_group );
					if ( post_type_exists( $post_type ) ) {
						$post_type_obj      = get_post_type_object( $post_type );
						$learndash_groups[] = [
							'value' => $ld_group,
							/* translators: %s post type label. */
							'label' => sprintf( __( 'All %s', 'suremembers' ), $post_type_obj->labels->name ?? $post_type ),
						];
					}
				}
			}

			// Process TutorLMS courses data to include post titles.
			$tutorlms_courses      = [];
			$tutorlms_courses_data = is_array( $include_data ) && ! empty( $include_data['tutorlms_courses'] ) ? $include_data['tutorlms_courses'] : [];
			foreach ( $tutorlms_courses_data as $tutor_course ) {
				// Handle 'post-123-|' format.
				if ( preg_match( '/(post(?:child)?)-(\d+)-\|?/', $tutor_course, $matches ) ) {
					$prefix   = $matches[1];
					$real_id  = intval( $matches[2] );
					$post_obj = get_post( $real_id );
					if ( $post_obj ) {
						$tutorlms_courses[] = [
							'id'    => $post_obj->ID,
							'title' => $prefix === 'postchild' ? sprintf( /* translators: %s title. */ __( 'Child of %s', 'suremembers' ), get_the_title( $post_obj ) ) : get_the_title( $post_obj ),
							'type'  => get_post_type( $post_obj ),
							'link'  => get_edit_post_link( $post_obj->ID ),
							'value' => $tutor_course,
						];
					}
				} elseif ( strpos( $tutor_course, '|all' ) !== false ) {
					// Handle 'courses|all' format.
					$post_type = str_replace( '|all', '', $tutor_course );
					if ( post_type_exists( $post_type ) ) {
						$post_type_obj      = get_post_type_object( $post_type );
						$tutorlms_courses[] = [
							'value' => $tutor_course,
							/* translators: %s post type label. */
							'label' => sprintf( __( 'All %s', 'suremembers' ), $post_type_obj->labels->name ?? $post_type ),
						];
					}
				}
			}

			// Process TutorLMS lessons data to include post titles.
			$tutorlms_lessons      = [];
			$tutorlms_lessons_data = is_array( $include_data ) && ! empty( $include_data['tutorlms_lessons'] ) ? $include_data['tutorlms_lessons'] : [];
			foreach ( $tutorlms_lessons_data as $tutor_lesson ) {
				// Handle 'post-123-|' format.
				if ( preg_match( '/(post(?:child)?)-(\d+)-\|?/', $tutor_lesson, $matches ) ) {
					$prefix   = $matches[1];
					$real_id  = intval( $matches[2] );
					$post_obj = get_post( $real_id );
					if ( $post_obj ) {
						$tutorlms_lessons[] = [
							'id'    => $post_obj->ID,
							'title' => $prefix === 'postchild' ? sprintf( /* translators: %s title. */ __( 'Child of %s', 'suremembers' ), get_the_title( $post_obj ) ) : get_the_title( $post_obj ),
							'type'  => get_post_type( $post_obj ),
							'link'  => get_edit_post_link( $post_obj->ID ),
							'value' => $tutor_lesson,
						];
					}
				} elseif ( strpos( $tutor_lesson, '|all' ) !== false ) {
					// Handle 'lesson|all' format.
					$post_type = str_replace( '|all', '', $tutor_lesson );
					if ( post_type_exists( $post_type ) ) {
						$post_type_obj      = get_post_type_object( $post_type );
						$tutorlms_lessons[] = [
							'value' => $tutor_lesson,
							/* translators: %s post type label. */
							'label' => sprintf( __( 'All %s', 'suremembers' ), $post_type_obj->labels->name ?? $post_type ),
						];
					}
				}
			}

			// Get user count (matches admin table logic).
			$required_fetch = get_post_meta( $post_id, SUREMEMBERS_REQUIRES_QUERY, true );
			if ( ! empty( $required_fetch ) ) {
				$users_in_access_group = \SureMembers\Inc\Access_Groups::get_users_count( $post_id );
			} else {
				$users_in_access_group = absint( get_post_meta( $post_id, SUREMEMBERS_PLAN_ACTIVE_USERS, true ) );
			}

			$post_data = [
				'id'                 => $post->ID,
				'title'              => $post->post_title,
				'status'             => $post->post_status,
				'priority'           => $priority_data,
				'expiration'         => $expiration_data,
				'date'               => get_the_date( 'c', $post ),
				'rules'              => $rules_data,
				'edit_link'          => \SureMembers\Inc\Access_Groups::get_admin_url(
					[
						'membership-id' => $post_id,
					]
				),
				'include'            => $include_data,
				'exclude'            => $exclude_data, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Array key for data structure, not WP_Query parameter.
				'include_posts'      => $include_posts,
				'exclude_posts'      => $exclude_posts,
				'learndash_courses'  => $learndash_courses,
				'learndash_groups'   => $learndash_groups,
				'tutorlms_courses'   => $tutorlms_courses,
				'tutorlms_lessons'   => $tutorlms_lessons,
				'user_count'         => $users_in_access_group,
				'restricted_url'     => $restricted_url_data,
				'user_roles'         => $user_roles_data,
				'drips'              => $processed_drips,
				'downloads'          => $downloads_data,
				'meta'               => [
					'restrict' => $rules_data,
				],
				'user_filter_url'    => add_query_arg(
					[
						'suremembers_access_group_top'    => $post_id,
						'suremembers_access_group_bottom' => $post_id,
					],
					admin_url( 'users.php' )
				),
				'specific_locations' => $specific_locations,
			];

			return [
				'success' => true,
				'data'    => $post_data,
			];
		} catch ( \Exception $e ) {
			return [
				'success' => false,
				'message' => $e->getMessage(),
			];
		}
	}

	/**
	 * AJAX handler to get post data for editing access groups (uses get_post_data_result).
	 *
	 * @since 2.0.0
	 */
	public function get_post_data_ajax(): void {
		$post_id = isset( $_POST['post_id'] ) ? absint( sanitize_text_field( $_POST['post_id'] ) ) : 0; //phpcs:ignore WordPress.Security.NonceVerification.Missing
		$result  = $this->get_post_data_result( $post_id );

		if ( $result['success'] ) {
			wp_send_json_success( $result['data'] ?? [] );
		} else {
			wp_send_json_error( [ 'message' => $result['message'] ?? '' ] );
		}
	}

	/**
	 * Update access group status (returns data instead of echoing JSON).
	 *
	 * @param int    $id Access group post ID.
	 * @param string $status New status ('publish', 'archive', or 'delete').
	 *
	 * @return array{success: bool, data?: array<string, mixed>, message?: string} Result array.
	 *
	 * @since 1.10.14
	 */
	public function update_access_group_status_data( $id, $status ): array {
		if ( empty( $id ) ) {
			return [
				'success' => false,
				'message' => __( 'Invalid post id.', 'suremembers' ),
			];
		}

		if ( empty( $status ) ) {
			return [
				'success' => false,
				'message' => __( 'Invalid data.', 'suremembers' ),
			];
		}

		$message  = '';
		$response = false;

		switch ( $status ) {
			case 'publish':
				wp_publish_post( $id );
				$response = true;
				$message  = __( 'Published successfully', 'suremembers' );
				break;
			case 'archive':
				$response = wp_update_post(
					[
						'ID'          => $id,
						'post_status' => SUREMEMBERS_ARCHIVE,
					]
				);
				$status   = 'suremembers_archive';
				if ( ! is_int( $response ) ) {
					$message = __( 'Updating failed', 'suremembers' );
				} else {
					$message  = __( 'Archived successfully', 'suremembers' );
					$response = true;
				}
				break;
			case 'delete':
				$response = wp_delete_post( $id );
				if ( $response ) {
					$message = __( 'Deleted successfully', 'suremembers' );
				} else {
					$message = __( 'Delete operation failed', 'suremembers' );
				}
				break;
			default:
				$response = false;
				$message  = __( 'Invalid status.', 'suremembers' );
				break;
		}

		if ( $response ) {
			return [
				'success' => true,
				'data'    => [
					'message' => $message,
					'action'  => $status,
				],
			];
		}

		return [
			'success' => false,
			'message' => __( 'Status updating failed', 'suremembers' ),
			'action'  => $status,
		];
	}

	/**
	 * Updates status of access group (AJAX handler - uses update_access_group_status_data).
	 *
	 * @since 1.0.0
	 */
	public function update_access_group_status(): void {
		if ( ! isset( $_POST['id'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
			wp_send_json_error( [ 'message' => __( 'Missing Post ID.', 'suremembers' ) ] );
		}

		if ( ! isset( $_POST['status'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
			wp_send_json_error( [ 'message' => __( 'Missing Post status data.', 'suremembers' ) ] );
		}

		$id     = intval( sanitize_text_field( $_POST['id'] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Missing
		$status = sanitize_text_field( $_POST['status'] ); //phpcs:ignore WordPress.Security.NonceVerification.Missing

		$result = $this->update_access_group_status_data( $id, $status );

		if ( $result['success'] ) {
			wp_send_json_success( $result['data'] ?? [] );
		} else {
			wp_send_json_error(
				[
					'message' => $result['message'] ?? '',
					'action'  => $result['action'] ?? $status,
				]
			);
		}
	}

	/**
	 * Fetches all the posts belonging to selected post time in ( Rules Engine)
	 *
	 * @since 1.0.0
	 */
	public function fetch_posts(): void {

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Current user does not have required permission.', 'suremembers' ) ] );
		}

		if ( empty( $_POST['postType'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
			wp_send_json_error( [ 'message' => __( 'Invalid data.', 'suremembers' ) ] );
		}

		$post_type = sanitize_text_field( $_POST['postType'] ); //phpcs:ignore WordPress.Security.NonceVerification.Missing

		$args = [
			'post_type'   => $post_type,
			'post_status' => 'publish',
		];

		$posts = get_posts( $args );

		if ( empty( $posts ) ) {
			wp_send_json_error( [ 'message' => __( 'No post available for this post type.', 'suremembers' ) ] );
		}

		$response = [];
		foreach ( $posts as $post ) {
			$temp          = [];
			$temp['value'] = $post->ID;
			$temp['label'] = $post->post_title;
			$response[]    = $temp;
		}

		wp_send_json_success( [ 'posts' => $response ] );
	}

	/**
	 * Save access group form data (returns data instead of echoing JSON).
	 *
	 * @param array<string, mixed> $suremembers_post Form data array.
	 * @param int                  $suremembers_post_id Post ID (0 for new post).
	 * @param string               $mode Mode type ('admin' or 'iframe').
	 *
	 * @return array{success: bool, data?: array<string, mixed>, message?: string} Result array.
	 *
	 * @since 1.10.14
	 */
	public function submit_form_data( $suremembers_post, $suremembers_post_id = 0, $mode = 'admin' ): array {
		if ( empty( $suremembers_post ) ) {
			return [
				'success' => false,
				'message' => __( 'No data received.', 'suremembers' ),
			];
		}

		// Preserve HTML markup for preview content.
		$restrict_preview_content = isset( $suremembers_post['restrict']['preview_content'] ) ? wp_filter_post_kses( $suremembers_post['restrict']['preview_content'] ) : '';

		// Preserve HTML markup for restricted URL text.
		$redirect_url_text = isset( $suremembers_post['restricted_url'] ) ? wp_filter_post_kses( $suremembers_post['restricted_url'] ) : '';

		// Decode and sanitize redirect URL if present.
		if ( isset( $suremembers_post['restrict']['redirect_url'] ) ) {
			$suremembers_post['restrict']['redirect_url'] = esc_url_raw( urldecode( $suremembers_post['restrict']['redirect_url'] ) );
		}

		// Save switch values BEFORE remove_blank_array (which removes empty strings and '0' values).
		$excerpt_to_save     = isset( $suremembers_post['restrict']['excerpt'] ) ? sanitize_text_field( $suremembers_post['restrict']['excerpt'] ) : '0';
		$in_content_to_save  = isset( $suremembers_post['restrict']['in_content'] ) ? sanitize_text_field( $suremembers_post['restrict']['in_content'] ) : '0';
		$enablelogin_to_save = isset( $suremembers_post['restrict']['enablelogin'] ) ? sanitize_text_field( $suremembers_post['restrict']['enablelogin'] ) : '0';

		// Sanitize all data recursively.
		$post_data = Utils::sanitize_recursively( 'sanitize_text_field', $suremembers_post );

		// Save download_files BEFORE remove_blank_array (which removes empty strings).
		$download_files_to_save = $post_data['download_files'] ?? '';

		$post_data = Utils::remove_blank_array( $post_data );

		if ( empty( $post_data['title'] ) ) {
			return [
				'success' => false,
				'message' => __( 'Title can\'t be empty.', 'suremembers' ),
			];
		}

		if ( empty( $suremembers_post_id ) ) {
			// Check if a membership with the same title exists.
			$original_title = $post_data['title'];
			$final_title    = $original_title;
			$copy_number    = 0;

			// Keep checking until we find a unique title.
			while ( get_page_by_title( $final_title, OBJECT, SUREMEMBERS_POST_TYPE ) ) {
				$copy_number++;
				$final_title = $original_title . ' copy ' . $copy_number;
			}

			$new_post = [
				'post_title'   => $final_title,
				'post_content' => '',
				'post_status'  => isset( $post_data['status'] ) && $post_data['status'] === 'suremembers_archive' ? 'suremembers_archive' : 'publish',
				'post_type'    => SUREMEMBERS_POST_TYPE,
			];

			// Insert the post into the database.
			$suremembers_post_id = wp_insert_post( $new_post );
		} else {
			$post_array          = [
				'post_title' => $post_data['title'],
				'ID'         => $suremembers_post_id,
			];
			$suremembers_post_id = wp_update_post( $post_array );
		}

		if ( is_wp_error( $suremembers_post_id ) ) {
			return [
				'success' => false,
				'message' => $suremembers_post_id->get_error_message(),
			];
		}

		if ( ! is_int( $suremembers_post_id ) ) {
			return [
				'success' => false,
				'message' => __( 'Invalid post ID returned.', 'suremembers' ),
			];
		}

		unset( $post_data['title'] );

		$include = [];
		if ( ! empty( $post_data['rules'] ) ) {
			$include['rules'] = $post_data['rules'];
			unset( $post_data['rules'] );
		}

		if ( ! empty( $post_data['specifics'] ) ) {
			$include['specifics'] = $post_data['specifics'];
			unset( $post_data['specifics'] );
		}

		$include = apply_filters( 'suremembers_access_group_edit_metadata', $include, $post_data );

		update_post_meta( $suremembers_post_id, SUREMEMBERS_PLAN_INCLUDE, $include );

		// Add restriction url.
		$save_restrict_url = [];
		if ( ! empty( $post_data['restricted_url'] ) ) {
			$redirect_url_text                   = str_replace( ',', "\n", $redirect_url_text );
			$save_restrict_url['restricted_url'] = $redirect_url_text;
			if ( ! empty( $post_data['restricted_url_reg_exp'] ) ) {
				$save_restrict_url['regex'] = sanitize_text_field( $post_data['restricted_url_reg_exp'] );
			}
			update_post_meta( $suremembers_post_id, SUREMEMBERS_RESTRICTED_URL, $save_restrict_url );
		} else {
			update_post_meta( $suremembers_post_id, SUREMEMBERS_RESTRICTED_URL, '' );
		}

		$exclude = [];
		if ( ! empty( $post_data['exclude'] ) ) {
			$exclude['rules'] = $post_data['exclude'];
			unset( $post_data['exclude'] );
		}
		update_post_meta( $suremembers_post_id, SUREMEMBERS_PLAN_EXCLUDE, $exclude );

		$priority = ! empty( $post_data['priority'] ) ? $post_data['priority'] : '';
		unset( $post_data['priority'] );
		update_post_meta( $suremembers_post_id, SUREMEMBERS_PLAN_PRIORITY, $priority );

		// Update expiration.
		$expiration = ! empty( $post_data['expiration'] ) ? Utils::sanitize_recursively( 'sanitize_text_field', $post_data['expiration'] ) : '';
		unset( $post_data['expiration'] );
		update_post_meta( $suremembers_post_id, SUREMEMBERS_PLAN_EXPIRATION, $expiration );

		$drips = ! empty( $post_data['drips'] ) ? Utils::sanitize_recursively( 'sanitize_text_field', $post_data['drips'] ) : '';
		unset( $post_data['drips'] );
		update_post_meta( $suremembers_post_id, SUREMEMBERS_PLAN_DRIPS, $drips );

		// Save user roles.
		$roles = ! empty( $post_data['suremembers_user_roles'] ) ? $post_data['suremembers_user_roles'] : '';
		unset( $post_data['suremembers_user_roles'] );
		update_post_meta( $suremembers_post_id, SUREMEMBERS_USER_ROLES, $roles );

		// save download files - use the pre-saved value (before remove_blank_array).
		$downloads = ! empty( $download_files_to_save ) ? $download_files_to_save : '';

		unset( $post_data['download_files'] );
		update_post_meta( $suremembers_post_id, SUREMEMBERS_ACCESS_GROUP_DOWNLOADS, $downloads );

		// Add back the fields that were saved before remove_blank_array.
		$post_data['restrict']['preview_content'] = $restrict_preview_content;
		$post_data['restrict']['excerpt']         = $excerpt_to_save;
		$post_data['restrict']['in_content']      = $in_content_to_save;
		$post_data['restrict']['enablelogin']     = $enablelogin_to_save;

		update_post_meta( $suremembers_post_id, SUREMEMBERS_PLAN_RULES, $post_data );

		// Access group data can be accessed via parameters.
		do_action( 'suremembers_after_submit_form', $suremembers_post_id );

		return [
			'success' => true,
			'data'    => [
				'message' => __( 'Rule saved successfully.', 'suremembers' ),
				'id'      => $suremembers_post_id,
				'mode'    => $mode,
			],
		];
	}

	/**
	 * Saves Sure Members rules and updates meta accordingly (AJAX handler - uses submit_form_data).
	 *
	 * @since 1.0.0
	 */
	public function submit_form(): void {
		if ( empty( $_POST['suremembers_post'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
			wp_send_json_error( [ 'message' => __( 'No data received.', 'suremembers' ) ] );
		}

		// Get parameters from $_POST.
		$suremembers_post    = $_POST['suremembers_post']; //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing
		$suremembers_post_id = isset( $_POST['suremembers_post_id'] ) ? absint( sanitize_text_field( $_POST['suremembers_post_id'] ) ) : 0; //phpcs:ignore WordPress.Security.NonceVerification.Missing
		$mode                = isset( $_POST['mode'] ) && $_POST['mode'] === 'iframe' ? 'iframe' : 'admin'; //phpcs:ignore WordPress.Security.NonceVerification.Missing

		// Call the refactored method that returns data instead of echoing JSON.
		$result = $this->submit_form_data( $suremembers_post, $suremembers_post_id, $mode );

		if ( $result['success'] ) {
			wp_send_json_success( $result['data'] ?? [] );
		} else {
			wp_send_json_error( [ 'message' => $result['message'] ?? '' ] );
		}
	}

	/**
	 * Save downloads data for an access group (returns data instead of echoing JSON).
	 *
	 * @param array<string, mixed> $suremembers_post Form data array.
	 * @param int                  $suremembers_post_id Post ID.
	 *
	 * @return array{success: bool, data?: array<string, mixed>, message?: string} Result array.
	 *
	 * @since 1.10.14
	 */
	public function save_downloads_data( $suremembers_post, $suremembers_post_id ): array {
		if ( empty( $suremembers_post ) ) {
			return [
				'success' => false,
				'message' => __( 'No data received.', 'suremembers' ),
			];
		}

		if ( ! $suremembers_post_id ) {
			return [
				'success' => false,
				'message' => __( 'Save Membership to protect downloads.', 'suremembers' ),
			];
		}

		// Sanitize all data recursively.
		$post_data = Utils::sanitize_recursively( 'sanitize_text_field', $suremembers_post );
		$post_data = Utils::remove_blank_array( $post_data );

		// Save download files.
		$downloads = ! empty( $post_data['download_files'] ) ? $post_data['download_files'] : '';
		update_post_meta( $suremembers_post_id, SUREMEMBERS_ACCESS_GROUP_DOWNLOADS, $downloads );

		return [
			'success' => true,
			'data'    => [ 'message' => __( 'Changes updated.', 'suremembers' ) ],
		];
	}

	/**
	 * Saves the downloads data in an Access Group (AJAX handler - uses save_downloads_data).
	 *
	 * @since 1.3.1
	 */
	public function save_downloads(): void {
		if ( empty( $_POST['suremembers_post'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
			wp_send_json_error( [ 'message' => __( 'No data received.', 'suremembers' ) ] );
		}

		$suremembers_post    = $_POST['suremembers_post']; //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing
		$suremembers_post_id = isset( $_POST['suremembers_post_id'] ) ? absint( sanitize_text_field( $_POST['suremembers_post_id'] ) ) : 0; //phpcs:ignore WordPress.Security.NonceVerification.Missing

		// Call the refactored method that returns data instead of echoing JSON.
		$result = $this->save_downloads_data( $suremembers_post, $suremembers_post_id );

		if ( $result['success'] ) {
			wp_send_json_success( $result['data'] ?? [] );
		} else {
			wp_send_json_error( [ 'message' => $result['message'] ?? '' ] );
		}
	}

	/**
	 * Get location selection options.
	 *
	 * @return array<string, mixed>
	 *
	 * @since 1.0.0
	 */
	public function get_location_selections(): array {
		$post_types = Restricted::get_post_types( 'object' );

		$selection_options = [
			'basic' => [
				'label' => __( 'Basic', 'suremembers' ),
				'value' => [
					'basic-global' => __( 'Entire Website', 'suremembers' ),
				],
			],
		];

		$args = [
			'public' => true,
		];

		$taxonomies = get_taxonomies( $args, 'objects' );
		unset( $taxonomies['post_format'] );

		if ( ! empty( $taxonomies ) ) {
			foreach ( $taxonomies as $taxonomy ) {
				$attached_post_types = $this->get_post_types_by_taxonomy( $taxonomy->name );

				foreach ( $post_types as $post_type ) {
					$post_opt = $this->get_post_target_rule_options( $post_type, $taxonomy, $attached_post_types );

					if ( isset( $selection_options[ $post_opt['post_key'] ] ) ) {
						if ( ! empty( $post_opt['value'] ) && is_array( $post_opt['value'] ) ) {
							foreach ( $post_opt['value'] as $key => $value ) {
								if ( ! in_array( $value, $selection_options[ $post_opt['post_key'] ]['value'], true ) ) {
									$selection_options[ $post_opt['post_key'] ]['value'][ $key ] = $value;
								}
							}
						}
					} else {
						$selection_options[ $post_opt['post_key'] ] = [
							'label' => $post_opt['label'],
							'value' => $post_opt['value'],
						];
					}
				}
			}
		}

		$selection_options['specific-target'] = [
			'label' => __( 'Specific Target', 'suremembers' ),
			'value' => [
				'specifics' => __( 'Specific Pages / Posts / Taxonomies, etc.', 'suremembers' ),
			],
		];

		// Restrict by specific Url.
		$selection_options['specific-url'] = [
			'label' => __( 'Specific Url', 'suremembers' ),
			'value' => [
				'restricted_url' => __( 'URL matching', 'suremembers' ),
			],
		];

		return apply_filters( 'suremembers_location_selection_options', $selection_options );
	}

	/**
	 * Get post type by taxonomy
	 *
	 * @param string $taxonomy taxonomy slug.
	 *
	 * @return array<string, mixed>
	 *
	 * @since 1.0.0
	 */
	public function get_post_types_by_taxonomy( $taxonomy = '' ): array {
		global $wp_taxonomies;
		if ( isset( $wp_taxonomies[ $taxonomy ] ) ) {
			return $wp_taxonomies[ $taxonomy ]->object_type;
		}
		return [];
	}

	/**
	 * Fetches posts related options for select array
	 *
	 * @param object               $post_type post object.
	 * @param object               $taxonomy taxonomy object.
	 * @param array<string, mixed> $attached_post_types posts attached to current taxonomy.
	 *
	 * @return array<string, mixed>
	 *
	 * @since 1.0.0
	 */
	public function get_post_target_rule_options( $post_type, $taxonomy, $attached_post_types ): array {
		$label       = $post_type->label ?? '';
		$post_key    = str_replace( ' ', '-', strtolower( $label ) );
		$post_label  = ucwords( $label );
		$post_name   = $post_type->name ?? '';
		$post_option = [];

		/* translators: %s post label */
		$all_posts                          = sprintf( __( 'All %s', 'suremembers' ), $post_label );
		$post_option[ $post_name . '|all' ] = $all_posts;

		if ( in_array( $post_name, $attached_post_types, true ) ) {
			$tax_label = ! empty( $taxonomy->label ) ? ucwords( $taxonomy->label ) : '';
			$tax_name  = ! empty( $taxonomy->name ) ? $taxonomy->name : '';

			/* translators: %s taxonomy label */
			$tax_archive = sprintf( __( 'All %s Archive', 'suremembers' ), $tax_label );

			$post_option[ $post_name . '|all|taxarchive|' . $tax_name ] = $tax_archive;
		}

		return [
			'post_key' => $post_key,
			'label'    => $post_label,
			'value'    => $post_option,
		];
	}

	/**
	 * Get posts by search query (returns data instead of echoing JSON).
	 *
	 * @param string $search_string Search query string.
	 * @param string $include Comma-separated list of post types/taxonomies to include.
	 * @param string $context Context for the search ('search' by default).
	 *
	 * @return array{success: bool, data?: array<string, mixed>, message?: string} Result array.
	 *
	 * @since 1.10.14
	 */
	public function get_posts_by_query_data( $search_string = '', $include = '', $context = 'search' ): array {
		if ( ! current_user_can( 'manage_options' ) ) {
			return [
				'success' => false,
				'message' => __( 'Current user does not have required permission.', 'suremembers' ),
			];
		}

		$data               = [];
		$result             = [];
		$includes_array     = explode( ',', $include );
		$post_types         = [];
		$include_taxonomies = [];
		if ( ! empty( $includes_array ) ) {
			foreach ( $includes_array as $rules ) {
				/**
				 * Added filter for learndash legacy value of `learndash-courses` and `learndash-groups`.
				 * Using this filter is discouraged, it is used for backward compatibilty and will be removed in future
				 *
				 * @since 1.4.0
				 */
				$rules  = apply_filters( 'suremembers_before_search_rules', $rules );
				$option = explode( '|', $rules );
				if ( count( $option ) > 1 ) {
					$temp                     = get_post_type_object( $option[0] );
					$post_types[ $option[0] ] = [ 'label' => $temp->label ?? '' ];
				}
				if ( count( $option ) === 4 ) {
					$post_types[ $option[0] ]['taxonomy'][] = $option[3];
					$include_taxonomies[]                   = $option[3];
				}
			}
		}

		if ( empty( $post_types ) ) {
			$post_types = Restricted::get_post_types( 'object', $context );
		}

		foreach ( $post_types as $key => $post_type ) {
			$data       = [];
			$child_data = [];

			add_filter( 'posts_search', [ $this, 'search_only_titles' ], 10, 2 );

			$query = new \WP_Query(
				[
					's'              => $search_string,
					'post_type'      => $key,
					'posts_per_page' => -1,
				]
			);

			if ( $query->have_posts() ) {
				// Check post is hierarchical or not.
				$check_hierarchical = is_post_type_hierarchical( $key );
				while ( $query->have_posts() ) {
					$query->the_post();
					$title  = get_the_title();
					$id     = get_the_id();
					$data[] = [
						'value' => 'post-' . $id . '-|',
						'label' => $title,
					];

					if ( $check_hierarchical ) {
						/* translators: %s title. */
						$children_title = sprintf( __( 'Child of %s', 'suremembers' ), $title );
						$child_data[]   = [
							'value' => 'postchild-' . $id . '-|',
							'label' => $children_title,
						];
					}
				}
			}

			$post_type = (array) $post_type;

			if ( ! empty( $data ) ) {
				$result[] = [
					'label'   => $post_type['label'],
					'options' => $data,
				];
			}
			if ( ! empty( $child_data ) ) {
				$result[] = [
					/* translators: %s label. */
					'label'   => sprintf( __( 'Child of %s', 'suremembers' ), $post_type['label'] ),
					'options' => $child_data,
				];
			}
		}

		$data = [];

		wp_reset_postdata();

		$args = [
			'public' => true,
		];

		$output     = 'objects'; // names or objects, note names is the default.
		$operator   = 'and';
		$taxonomies = get_taxonomies( $args, $output, $operator );

		foreach ( $taxonomies as $tax => $taxonomy ) {
			if ( ! empty( $include_taxonomies ) && ! in_array( $tax, $include_taxonomies, true ) ) {
				continue;
			}

			if ( empty( $include_taxonomies ) ) {
				$attached_post_types = $this->get_post_types_by_taxonomy( $taxonomy->name );

				if ( empty( array_intersect( $attached_post_types, array_keys( $post_types ) ) ) ) {
					continue;
				}
			}

			$terms = get_terms(
				[
					'taxonomy'   => $taxonomy->name,
					'orderby'    => 'count',
					'hide_empty' => 0,
					'name__like' => $search_string,
				]
			);

			$data = [];

			$label = ucwords( $taxonomy->label );

			if ( ! empty( $terms ) && is_array( $terms ) ) {
				foreach ( $terms as $term ) {
					$data[] = [
						'value' => 'tax-' . $term->term_id . '-single-' . $taxonomy->name,
						'label' => 'All singulars from ' . $term->name,
					];
				}
			}

			if ( is_array( $data ) && ! empty( $data ) ) {
				$result[] = [
					'label'   => $label,
					'options' => $data,
				];
			}
		}
		// Return the result as array.
		return [
			'success' => true,
			'data'    => $result,
		];
	}

	/**
	 * Returns content as per search string (AJAX handler - uses get_posts_by_query_data).
	 *
	 * @since 1.0.0
	 */
	public function get_posts_by_query(): void {
		$search_string = isset( $_POST['q'] ) ? sanitize_text_field( $_POST['q'] ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Missing
		$include       = ! empty( $_POST['include'] ) ? sanitize_text_field( $_POST['include'] ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Missing
		$context       = ! empty( $_POST['context'] ) ? sanitize_text_field( $_POST['context'] ) : 'search'; //phpcs:ignore WordPress.Security.NonceVerification.Missing

		$result = $this->get_posts_by_query_data( $search_string, $include, $context );

		if ( $result['success'] ) {
			wp_send_json_success( $result['data'] ?? [] );
		} else {
			wp_send_json_error( [ 'message' => $result['message'] ?? '' ] );
		}
	}

	/**
	 * Modifies search query to search only title
	 *
	 * @param string $search search string.
	 * @param object $wp_query WP_QUERY object.
	 *
	 * @since 1.0.0
	 */
	public function search_only_titles( $search, $wp_query ): string {
		if ( ! empty( $search ) && ! empty( $wp_query->query_vars['search_terms'] ) ) {
			global $wpdb;

			$q = $wp_query->query_vars;
			$n = ! empty( $q['exact'] ) ? '' : '%';

			$search = [];

			foreach ( $q['search_terms'] as $term ) {
				$search[] = $wpdb->prepare( "{$wpdb->posts}.post_title LIKE %s", $n . $wpdb->esc_like( $term ) . $n );
			}

			if ( ! is_user_logged_in() ) {
				$search[] = "{$wpdb->posts}.post_password = ''";
			}

			$search = ' AND ' . implode( ' AND ', $search );
		}

		return $search;
	}

	/**
	 * Get user selected values
	 *
	 * @param mixed $post_id post id to retrieve data from.
	 *
	 * @return array<string, mixed>
	 *
	 * @since 1.0.0
	 */
	public function get_selected_locations( $post_id = false ): array {
		if ( empty( $post_id ) ) {
			// Ignored nonce verification as we are getting post_id from URL.
			if ( empty( $_GET['post_id'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
				return [];
			}
			// Ignored nonce verification as we are getting post_id from URL.
			$post_id = absint( $_GET['post_id'] ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}

		$id = absint( $post_id );

		$includes = get_post_meta( $id, SUREMEMBERS_PLAN_INCLUDE, true );
		$rules    = is_array( $includes ) && ! empty( $includes['rules'] ) ? Utils::sanitize_recursively( 'sanitize_text_field', $includes['rules'] ) : [];

		/**
		 * Added filter for learndash legacy value of `learndash-courses` and `learndash-groups`.
		 * Using this filter is discouraged, it is used for backward compatibilty and will be removed in future
		 *
		 * @since 1.4.0
		 */
		$rules = apply_filters( 'suremembers_before_search_rules', $rules );
		return $rules;
	}

	/**
	 * Returns data of specific location i.e. specific, excluded.
	 *
	 * @param string $type type of data to be retrieved 'specific | excluded'.
	 * @param mixed  $post_id post id to retrieve data for.
	 *
	 * @return array<string, mixed>
	 *
	 * @since 1.0.0
	 */
	public function get_individual_data( $type = 'exclude', $post_id = false ): array {
		if ( empty( $post_id ) ) {
			// Ignored nonce verification as we are getting post_id from URL.
			if ( empty( $_GET['post_id'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
				return [];
			}

			// Ignored nonce verification as we are getting post_id from URL.
			$post_id = absint( $_GET['post_id'] ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}

		$id = absint( $post_id );

		switch ( $type ) {
			case 'specific':
				$includes = get_post_meta( $id, SUREMEMBERS_PLAN_INCLUDE, true );
				$data     = is_array( $includes ) && ! empty( $includes['specifics'] ) ? Utils::sanitize_recursively( 'sanitize_text_field', $includes['specifics'] ) : [];
				break;

			default:
				$exclude = get_post_meta( $id, SUREMEMBERS_PLAN_EXCLUDE, true );
				$data    = is_array( $exclude ) && ! empty( $exclude['rules'] ) ? Utils::sanitize_recursively( 'sanitize_text_field', $exclude['rules'] ) : [];
				break;
		}

		if ( empty( $data ) ) {
			return $data;
		}
		return Utils::convert_slug_to_text( $data );
	}

	/**
	 * Fetches current posts drip data
	 *
	 * @param mixed $post_id either provide post id to fetch drip data or by default will fetch data of current post.
	 *
	 * @return array<string, mixed>
	 *
	 * @since 1.0.0
	 */
	public function get_drip_data( $post_id = false ): array {
		if ( empty( $post_id ) ) {
			// Ignored nonce verification as we are getting post_id from URL.
			if ( empty( $_GET['post_id'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
				return [];
			}

			// Ignored nonce verification as we are getting post_id from URL.
			$post_id = absint( $_GET['post_id'] ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}

		$id = absint( $post_id );

		$drips_array = get_post_meta( $id, SUREMEMBERS_PLAN_DRIPS, true );

		if ( empty( $drips_array ) || ! is_array( $drips_array ) ) {
			return [];
		}

		$response = [];
		$i        = 0;
		foreach ( $drips_array as $drip ) {
			if ( empty( $drip['rules'] ) ) {
				continue;
			}

			$texts = Utils::convert_slug_to_text( $drip['rules'] );
			if ( empty( $texts ) ) {
				continue;
			}

			$response[ $i ]['rules']     = $texts;
			$response[ $i ]['date_type'] = ! empty( $drip['date_type'] ) ? $drip['date_type'] : 'after_duration';
			$response[ $i ]['drip_date'] = ! empty( $drip['drip_date'] ) ? $drip['drip_date'] : '';
			$response[ $i ]['delay']     = ! empty( $drip['delay'] ) ? intval( $drip['delay'] ) : 0;
			$response[ $i ]['time']      = isset( $drip['time'] ) && $drip['time'] >= 0 ? floatval( $drip['time'] ) : '';
			$response[ $i ]['periods']   = isset( $drip['periods'] ) && $drip['periods'] >= 0 ? intval( $drip['periods'] ) : '';
			$i++;
		}

		$delays = array_column( $response, 'delay' );
		array_multisort( $delays, SORT_ASC, $response );

		return $response;
	}

	/**
	 * Check for post status change
	 *
	 * @since 1.0.0
	 */
	public function check_status_transition(): void {
		if ( ! empty( $_GET['post'] ) && ! empty( $_GET['action'] ) && in_array( sanitize_text_field( $_GET['action'] ), [ SUREMEMBERS_ARCHIVE, SUREMEMBERS_ARCHIVE . '_revert' ], true ) && ! empty( $_GET['_wpnonce'] ) ) {
			$post_id = absint( $_GET['post'] );
			$action  = sanitize_text_field( $_GET['action'] );

			if ( ! wp_verify_nonce( sanitize_text_field( $_GET['_wpnonce'] ), sanitize_text_field( $action ) ) ) {
				return;
			}

			if ( SUREMEMBERS_ARCHIVE . '_revert' === $action ) {
				$status = 'publish';
			} else {
				$status = SUREMEMBERS_ARCHIVE;
			}

			$response = wp_update_post(
				[
					'ID'          => $post_id,
					'post_status' => $status,
				]
			);

			$query_args = [ 'post_type' => SUREMEMBERS_POST_TYPE ];

			if ( isset( $_GET['suremembers_view'] ) && $_GET['suremembers_view'] === 'iframe' ) {
				$query_args['suremembers_view'] = 'iframe';
			}

			if ( $response ) {
				wp_safe_redirect( add_query_arg( $query_args, admin_url( 'edit.php' ) ) );
				exit;
			}
		}
	}

	/**
	 * Url restriction data.
	 *
	 * @param int $post_id Current post id.
	 *
	 * @return array<string, mixed>
	 *
	 * @since 1.1.0
	 */
	public function get_restricted_url_data( $post_id = 0 ): array {
		if ( empty( $post_id ) ) {
			// Ignored nonce verification as we are getting post_id from URL.
			if ( empty( $_GET['post_id'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
				return [];
			}

			// Ignored nonce verification as we are getting post_id from URL.
			$post_id = absint( $_GET['post_id'] ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
		}

		$get_restricted_url = get_post_meta( $post_id, SUREMEMBERS_RESTRICTED_URL, true );
		$result             = [];
		if ( is_array( $get_restricted_url ) && ! empty( $get_restricted_url['restricted_url'] ) ) {
			$result['restricted_url'] = wp_kses_post( $get_restricted_url['restricted_url'] );
			if ( ! empty( $get_restricted_url['regex'] ) ) {
				$result['restricted_url_reg_exp'] = true;
			}
		}
		return $result;
	}

	/**
	 * Return redirection rules
	 *
	 * @return array<string, mixed>
	 *
	 * @since 1.3.0
	 */
	private function get_redirection_rules(): array {
		return \SureMembers\Inc\Settings::get_setting( SUREMEMBERS_REDIRECT_RULES );
	}

	/**
	 * Return selected access group in format required for react select.
	 *
	 * @since 1.4.0
	 */
	private function get_selected_registration_access_group(): array {
		$settings = \SureMembers\Inc\Settings::get_setting( SUREMEMBERS_ADMIN_SETTINGS );

		$access_group_ids = isset( $settings['registration_access_group'] ) ? Utils::sanitize_recursively( 'absint', $settings['registration_access_group'] ) : [];
		if ( empty( $access_group_ids ) ) {
			return [];
		}

		$access_groups = Access_Groups::get_active( [ 'post__in' => $access_group_ids ] );
		if ( empty( $access_groups ) ) {
			return [];
		}

		return Utils::get_react_select_format( $access_groups );
	}

	/**
	 * Get plugin status.
	 *
	 * @since 1.0.0
	 *
	 * @param  string $plugin_init_file Plugin init file.
	 * @return string Plugin status (active|inactive|not-installed).
	 */
	private function get_plugin_status( $plugin_init_file ): string {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$installed_plugins = get_plugins();

		if ( ! isset( $installed_plugins[ $plugin_init_file ] ) ) {
			return 'not-installed';
		}

		if ( is_plugin_active( $plugin_init_file ) ) {
			return 'active';
		}

		return 'inactive';
	}
}
