<?php

defined( 'ABSPATH' ) || exit;

/**
 * Menu Visibility module.
 *
 * Adds per-menu-item visibility controls to Appearance → Menus and filters
 * `wp_get_nav_menu_items` on the front-end so hidden items don't render.
 *
 * Four visibility modes per menu item:
 *  - everyone        (default — no restriction)
 *  - logged_in       (visible only to authenticated users)
 *  - logged_out      (visible only to guests — useful for Login/Register links)
 *  - specific_roles  (visible to logged-in users whose role matches a checked role)
 *
 * Hidden parents cascade to their children — so restricting a top-level item
 * also hides its descendants.
 *
 * Storage: per-menu-item post-meta `_bw_dev_menu_visibility` =
 *   array( 'mode' => <mode>, 'roles' => <array of role slugs> )
 * Items in the default 'everyone' state store no meta (cleaned on save).
 *
 * @package BW_Dev
 */

class BW_Dev_Module_Menu_Visibility implements BW_Dev_Module_Interface {

	const META_KEY       = '_bw_dev_menu_visibility';
	const ALLOWED_MODES  = array( 'everyone', 'logged_in', 'logged_out', 'specific_roles' );

	public function slug(): string {
		return 'menu_visibility';
	}

	public function label(): string {
		return __( 'Menu Visibility', 'bw-dev' );
	}

	public function group(): string {
		return 'frontend';
	}

	public function default_settings(): array {
		// All real configuration is per menu item, not per plugin.
		return array();
	}

	public function sanitize( array $data ): array {
		unset( $data );
		return array();
	}

	public function register(): void {
		// Admin: per-menu-item controls in Appearance → Menus.
		add_action( 'wp_nav_menu_item_custom_fields', array( $this, 'render_menu_item_fields' ), 10, 4 );
		add_action( 'wp_update_nav_menu_item',        array( $this, 'save_menu_item_meta' ),     10, 2 );
		add_action( 'admin_enqueue_scripts',          array( $this, 'enqueue_menu_editor_assets' ) );

		// Front-end: drop items the current user shouldn't see.
		add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_menu_items' ) );
	}

	/* ---------------------------------------------------------------------
	 * Menu editor UI
	 * ------------------------------------------------------------------- */

	/**
	 * Render the visibility controls under each menu item's accordion.
	 *
	 * @param int    $item_id The nav-menu-item post ID.
	 * @param object $item    Unused — full menu item object.
	 * @param int    $depth   Unused — depth in the menu tree.
	 * @param object $args    Unused — wp_nav_menu_item_custom_fields args.
	 */
	public function render_menu_item_fields( $item_id, $item, $depth, $args ): void {
		unset( $item, $depth, $args );

		$item_id = (int) $item_id;
		$vis     = get_post_meta( $item_id, self::META_KEY, true );
		$mode    = is_array( $vis ) && isset( $vis['mode'] ) ? (string) $vis['mode'] : 'everyone';
		$roles   = is_array( $vis ) && isset( $vis['roles'] ) && is_array( $vis['roles'] ) ? $vis['roles'] : array();
		if ( ! in_array( $mode, self::ALLOWED_MODES, true ) ) {
			$mode = 'everyone';
		}

		$field_id   = 'bw-dev-menu-vis-' . $item_id;
		$name_mode  = 'bw_dev_menu_visibility[' . $item_id . '][mode]';
		$name_roles = 'bw_dev_menu_visibility[' . $item_id . '][roles][]';
		$all_roles  = wp_roles()->get_names();
		?>
		<p class="field-bw-dev-menu-visibility description description-wide">
			<label for="<?php echo esc_attr( $field_id ); ?>">
				<?php esc_html_e( 'Visibility (BW Dev)', 'bw-dev' ); ?><br />
				<select id="<?php echo esc_attr( $field_id ); ?>" name="<?php echo esc_attr( $name_mode ); ?>" class="widefat bw-dev-menu-vis-mode">
					<option value="everyone"       <?php selected( $mode, 'everyone' ); ?>><?php esc_html_e( 'Everyone (default)', 'bw-dev' ); ?></option>
					<option value="logged_in"      <?php selected( $mode, 'logged_in' ); ?>><?php esc_html_e( 'Logged-in users only', 'bw-dev' ); ?></option>
					<option value="logged_out"     <?php selected( $mode, 'logged_out' ); ?>><?php esc_html_e( 'Logged-out visitors only', 'bw-dev' ); ?></option>
					<option value="specific_roles" <?php selected( $mode, 'specific_roles' ); ?>><?php esc_html_e( 'Specific roles', 'bw-dev' ); ?></option>
				</select>
			</label>
		</p>
		<p class="field-bw-dev-menu-visibility-roles description description-wide" style="<?php echo 'specific_roles' === $mode ? '' : 'display:none;'; ?>">
			<span><?php esc_html_e( 'Roles allowed:', 'bw-dev' ); ?></span><br />
			<?php foreach ( $all_roles as $role_slug => $role_name ) : ?>
				<label style="display:inline-block;margin-right:12px;">
					<input type="checkbox" name="<?php echo esc_attr( $name_roles ); ?>" value="<?php echo esc_attr( $role_slug ); ?>" <?php checked( in_array( $role_slug, $roles, true ) ); ?> />
					<?php echo esc_html( translate_user_role( $role_name ) ); ?>
				</label>
			<?php endforeach; ?>
		</p>
		<?php
	}

	/**
	 * Save submitted visibility meta on a menu item. Fired by core's
	 * `wp_update_nav_menu_item`, which runs AFTER the menu editor's nonce
	 * (`update-nav_menu`) has been verified by wp-admin/nav-menus.php.
	 *
	 * @param int $menu_id          Unused — the menu (term) ID.
	 * @param int $menu_item_db_id  The nav-menu-item post ID.
	 */
	public function save_menu_item_meta( $menu_id, $menu_item_db_id ): void {
		unset( $menu_id );

		if ( ! current_user_can( 'edit_theme_options' ) ) {
			return;
		}

		$menu_item_db_id = (int) $menu_item_db_id;
		if ( ! $menu_item_db_id ) {
			return;
		}

		// Nonce verified by core's wp-admin/nav-menus.php before this action fires.
		if ( ! isset( $_POST['bw_dev_menu_visibility'][ $menu_item_db_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			return;
		}
		$raw = wp_unslash( $_POST['bw_dev_menu_visibility'][ $menu_item_db_id ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( ! is_array( $raw ) ) {
			return;
		}

		$mode = isset( $raw['mode'] ) ? sanitize_key( $raw['mode'] ) : 'everyone';
		if ( ! in_array( $mode, self::ALLOWED_MODES, true ) ) {
			$mode = 'everyone';
		}

		$roles = array();
		if ( 'specific_roles' === $mode && isset( $raw['roles'] ) && is_array( $raw['roles'] ) ) {
			$valid_roles = array_keys( wp_roles()->get_names() );
			foreach ( $raw['roles'] as $role_slug ) {
				$role_slug = sanitize_key( (string) $role_slug );
				if ( in_array( $role_slug, $valid_roles, true ) ) {
					$roles[] = $role_slug;
				}
			}
			$roles = array_values( array_unique( $roles ) );
		}

		// Clean up DB when item is in the default state.
		if ( 'everyone' === $mode ) {
			delete_post_meta( $menu_item_db_id, self::META_KEY );
			return;
		}

		update_post_meta(
			$menu_item_db_id,
			self::META_KEY,
			array(
				'mode'  => $mode,
				'roles' => $roles,
			)
		);
	}

	/**
	 * Inline JS to show/hide the roles row when the mode dropdown changes.
	 * Loaded only on the nav-menus.php screen, attached to the core nav-menu
	 * script handle so it fires after WP's own menu editor JS is ready.
	 *
	 * @param string $hook_suffix Current admin page hook.
	 */
	public function enqueue_menu_editor_assets( $hook_suffix ): void {
		if ( 'nav-menus.php' !== $hook_suffix ) {
			return;
		}
		$js = <<<'JS'
(function () {
	document.addEventListener('change', function (e) {
		if (!e.target || !e.target.classList || !e.target.classList.contains('bw-dev-menu-vis-mode')) {
			return;
		}
		var item = e.target.closest('.menu-item-settings');
		if (!item) {
			return;
		}
		var rolesRow = item.querySelector('.field-bw-dev-menu-visibility-roles');
		if (rolesRow) {
			rolesRow.style.display = (e.target.value === 'specific_roles') ? '' : 'none';
		}
	});
})();
JS;
		wp_add_inline_script( 'nav-menu', $js );
	}

	/* ---------------------------------------------------------------------
	 * Front-end filtering
	 * ------------------------------------------------------------------- */

	/**
	 * Drop hidden items from a menu before rendering. Hidden parents cascade
	 * to their children (relies on wp_get_nav_menu_items returning items in
	 * menu_order, which puts parents before their descendants).
	 *
	 * Skipped in the admin (so the menu editor sees every item) and in REST
	 * requests (so the block/site editor previews show every item).
	 *
	 * @param mixed $items Array of menu item objects, or anything else WP hands us.
	 * @return mixed
	 */
	public function filter_menu_items( $items ) {
		if ( ! is_array( $items ) || empty( $items ) ) {
			return $items;
		}
		if ( is_admin() ) {
			return $items;
		}
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return $items;
		}

		$hidden_ids = array();
		$visible    = array();
		foreach ( $items as $item ) {
			$own_hidden    = ! self::user_can_see( get_post_meta( (int) $item->ID, self::META_KEY, true ) );
			$parent_hidden = ! empty( $item->menu_item_parent ) && in_array( (int) $item->menu_item_parent, $hidden_ids, true );
			if ( $own_hidden || $parent_hidden ) {
				$hidden_ids[] = (int) $item->ID;
				continue;
			}
			$visible[] = $item;
		}
		return $visible;
	}

	/**
	 * Whether the current user passes a single item's visibility rule.
	 *
	 * @param mixed $vis Stored meta value (array, or anything else if absent/corrupt).
	 */
	private static function user_can_see( $vis ): bool {
		if ( ! is_array( $vis ) || empty( $vis['mode'] ) ) {
			return true;
		}
		$mode = (string) $vis['mode'];
		if ( 'everyone' === $mode ) {
			return true;
		}
		$logged_in = is_user_logged_in();
		switch ( $mode ) {
			case 'logged_in':
				return $logged_in;
			case 'logged_out':
				return ! $logged_in;
			case 'specific_roles':
				if ( ! $logged_in ) {
					return false;
				}
				$allowed = isset( $vis['roles'] ) && is_array( $vis['roles'] ) ? $vis['roles'] : array();
				if ( empty( $allowed ) ) {
					// Mode picked but no roles ticked — fall through to "any logged-in user".
					return true;
				}
				$user_roles = (array) wp_get_current_user()->roles;
				return (bool) array_intersect( $user_roles, $allowed );
			default:
				return true;
		}
	}

	/* ---------------------------------------------------------------------
	 * Settings tab (informational only)
	 * ------------------------------------------------------------------- */

	public function render_tab(): void {
		?>
		<p class="description">
			<?php esc_html_e( 'Hide individual menu items based on whether the visitor is logged in, logged out, or has a specific role.', 'bw-dev' ); ?>
		</p>
		<p>
			<?php esc_html_e( 'Edit any menu and you will see a new', 'bw-dev' ); ?>
			<strong><?php esc_html_e( 'Visibility (BW Dev)', 'bw-dev' ); ?></strong>
			<?php esc_html_e( 'control on every menu item.', 'bw-dev' ); ?>
			<a href="<?php echo esc_url( admin_url( 'nav-menus.php' ) ); ?>"><?php esc_html_e( 'Open the menu editor →', 'bw-dev' ); ?></a>
		</p>

		<h3><?php esc_html_e( 'How it works', 'bw-dev' ); ?></h3>
		<ul style="list-style:disc;margin-left:20px;">
			<li><strong><?php esc_html_e( 'Everyone (default):', 'bw-dev' ); ?></strong> <?php esc_html_e( 'No restriction — every visitor sees this item.', 'bw-dev' ); ?></li>
			<li><strong><?php esc_html_e( 'Logged-in users only:', 'bw-dev' ); ?></strong> <?php esc_html_e( 'Hidden from logged-out visitors.', 'bw-dev' ); ?></li>
			<li><strong><?php esc_html_e( 'Logged-out visitors only:', 'bw-dev' ); ?></strong> <?php esc_html_e( 'Hidden from anyone who is signed in (useful for Login or Register links).', 'bw-dev' ); ?></li>
			<li><strong><?php esc_html_e( 'Specific roles:', 'bw-dev' ); ?></strong> <?php esc_html_e( 'Visible only to logged-in users whose role matches one of the checked roles. If no role is checked, visible to all logged-in users.', 'bw-dev' ); ?></li>
		</ul>
		<p class="description">
			<?php esc_html_e( 'When a parent menu item is hidden, its children are hidden too — so you only need to set visibility on the top-level item in most cases.', 'bw-dev' ); ?>
		</p>
		<p class="description">
			<?php esc_html_e( 'Filtering is skipped in the admin (so the menu editor shows every item) and in the block/site editor preview, so admins always retain full visibility while managing menus.', 'bw-dev' ); ?>
		</p>
		<?php
	}

	public function uninstall(): void {
		global $wpdb;
		// Drop every per-item visibility meta. Safe to run on any postmeta table
		// because the key is namespaced.
		$wpdb->delete( $wpdb->postmeta, array( 'meta_key' => self::META_KEY ), array( '%s' ) ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
	}
}
