<?php
/**
 * Plugin Access Control
 *
 * Manages the three mutually exclusive access modes (default, admin_only, cap_only)
 * that govern who can see and use the Menu Organizer plugin UI.
 *
 * @package Agency_Pulse_Admin_Menu
 * @since   1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Agency_Pulse_Admin_Menu_Access_Control {

	// -------------------------------------------------------------------------
	// Initialisation
	// -------------------------------------------------------------------------

	/**
	 * Register WordPress hooks.
	 *
	 * Must be called during plugins_loaded so the user_has_cap filter is in place
	 * before any capability checks run (e.g. during admin_menu).
	 *
	 * @since 1.0.0
	 */
	public static function init(): void {
		if ( self::get_mode() === Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_ADMIN ) {
			add_filter( 'user_has_cap', array( __CLASS__, 'filter_admin_only_mode' ), 10, 4 );
		}

		// Failsafe recovery — must be registered regardless of current mode.
		add_action( 'admin_init', array( __CLASS__, 'maybe_trigger_failsafe' ) );
	}

	/**
	 * Failsafe capability recovery.
	 *
	 * When ?ap_menu_manager=ap_menu_manager_ap_menu_manager is present in an admin
	 * URL, resets access mode to 'default' and restores ap_menu_manager on the
	 * administrator role so a locked-out admin can regain access.
	 *
	 * Only users who have the 'administrator' role (checked directly on the role
	 * list, bypassing the user_has_cap filter) or who are multisite super admins
	 * may trigger this action.
	 *
	 * @since 1.0.0
	 */
	public static function maybe_trigger_failsafe(): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['ap_menu_manager'] ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( 'ap_menu_manager_ap_menu_manager' !== $_GET['ap_menu_manager'] ) {
			return;
		}

		// Authorise: must have the administrator role directly (or be a multisite super admin).
		// We intentionally avoid current_user_can() here because the user_has_cap
		// filter may have suppressed ap_menu_manager for this user — that is the
		// very condition we need to recover from.
		$current_user = wp_get_current_user();
		$is_authorised = ( $current_user->exists() && in_array( 'administrator', (array) $current_user->roles, true ) )
			|| ( is_multisite() && is_super_admin( $current_user->ID ) );

		if ( ! $is_authorised ) {
			return;
		}

		// Restore the capability on the administrator role.
		$admin_role = get_role( 'administrator' );
		if ( $admin_role ) {
			$admin_role->add_cap( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS );
		}

		// Reset mode to default.
		update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ACCESS_MODE, Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_DEFAULT );

		// Redirect to the same page without the query string parameter.
		wp_safe_redirect( remove_query_arg( 'ap_menu_manager' ) );
		exit;
	}

	/**
	 * user_has_cap filter for admin_only mode.
	 *
	 * Suppresses ap_menu_manager for users who are not in the administrator role.
	 * Super admins on multisite are always exempt.
	 *
	 * @since 1.0.0
	 * @param array   $allcaps All capabilities the user has.
	 * @param array   $caps    Primitive capabilities being checked.
	 * @param array   $args    Additional arguments (capability, user ID, object ID).
	 * @param WP_User $user    The user object.
	 * @return array
	 */
	public static function filter_admin_only_mode( array $allcaps, array $caps, array $args, WP_User $user ): array {
		if ( in_array( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS, $caps, true )
			 && ! in_array( 'administrator', $user->roles, true )
			 && ! ( is_multisite() && is_super_admin( $user->ID ) ) ) {
			$allcaps[ Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ] = false;
		}
		return $allcaps;
	}

	// -------------------------------------------------------------------------
	// Mode read / write
	// -------------------------------------------------------------------------

	/**
	 * Return the current access mode.
	 *
	 * Falls back to 'default' if the option is missing or invalid.
	 *
	 * @since 1.0.0
	 * @return string One of the ACCESS_MODE_* constants.
	 */
	public static function get_mode(): string {
		$mode  = (string) get_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ACCESS_MODE, Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_DEFAULT );
		$valid = array(
			Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_DEFAULT,
			Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_ADMIN,
			Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY,
		);
		return in_array( $mode, $valid, true ) ? $mode : Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_DEFAULT;
	}

	/**
	 * Persist a new access mode and apply any required role cap mutations.
	 *
	 * Transition rules:
	 *   Leaving cap_only  → always re-add ap_menu_manager to the administrator role first.
	 *   Entering cap_only → remove ap_menu_manager from the administrator role.
	 *
	 * @since 1.0.0
	 * @param string $new_mode One of the ACCESS_MODE_* constants.
	 * @return bool True on success, false if $new_mode is invalid.
	 */
	public static function set_mode( string $new_mode ): bool {
		$valid = array(
			Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_DEFAULT,
			Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_ADMIN,
			Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY,
		);

		if ( ! in_array( $new_mode, $valid, true ) ) {
			return false;
		}

		$current_mode = self::get_mode();
		$admin_role   = get_role( 'administrator' );

		// Leaving cap_only: restore the administrator role cap before applying new mode rules.
		if ( Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY === $current_mode
			 && Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY !== $new_mode ) {
			if ( $admin_role ) {
				$admin_role->add_cap( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS );
			}
		}

		// Entering cap_only: remove cap from the administrator role so WordPress enforces it natively.
		if ( Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY === $new_mode ) {
			if ( $admin_role ) {
				$admin_role->remove_cap( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS );
			}
		}

		return (bool) update_option( Agency_Pulse_Admin_Menu_Constants::OPTION_ACCESS_MODE, $new_mode );
	}

	// -------------------------------------------------------------------------
	// Explicit holder management
	// -------------------------------------------------------------------------

	/**
	 * Return all users who have an explicit individual ap_menu_manager grant.
	 *
	 * Queries by usermeta rather than loading all users so the lookup stays
	 * efficient on large sites. A PHP-level cap check is added as a safeguard.
	 *
	 * @since 1.0.0
	 * @return WP_User[]
	 */
	public static function get_explicit_holders(): array {
		global $wpdb;

		$meta_key = $wpdb->get_blog_prefix() . 'capabilities';

		$query = new WP_User_Query( array(
			'meta_query' => array(
				array(
					'key'     => $meta_key,
					'value'   => '"' . Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS . '";b:1',
					'compare' => 'LIKE',
				),
			),
			'fields' => 'all',
		) );

		$results = array();
		foreach ( $query->get_results() as $user ) {
			// Verify via $user->caps (user-level only, excludes role inheritance).
			if ( ! empty( $user->caps[ Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS ] ) ) {
				$results[] = $user;
			}
		}

		return $results;
	}

	/**
	 * Grant ap_menu_manager to a user via an explicit individual cap.
	 *
	 * @since 1.0.0
	 * @param int $user_id WordPress user ID.
	 * @return bool False if the user does not exist.
	 */
	public static function grant( int $user_id ): bool {
		$user = get_user_by( 'id', $user_id );
		if ( ! $user ) {
			return false;
		}
		$user->add_cap( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS );
		return true;
	}

	/**
	 * Revoke the explicit ap_menu_manager grant from a user.
	 *
	 * Blocked in cap_only mode when this user is the last explicit holder, to
	 * prevent a full lockout of the plugin.
	 *
	 * @since 1.0.0
	 * @param int $user_id WordPress user ID.
	 * @return bool|WP_Error True on success, false if user not found,
	 *                        WP_Error if the revoke would lock out all users.
	 */
	public static function revoke( int $user_id ) {
		if ( Agency_Pulse_Admin_Menu_Constants::ACCESS_MODE_CAP_ONLY === self::get_mode() ) {
			$holders = self::get_explicit_holders();
			if ( 1 === count( $holders ) && (int) $holders[0]->ID === $user_id ) {
				return new WP_Error(
					'last_holder',
					__( 'Cannot revoke the last user\'s access while in Capability Only mode. Switch to a different mode first or grant access to another user.', 'agency-pulse-admin-menu' )
				);
			}
		}

		$user = get_user_by( 'id', $user_id );
		if ( ! $user ) {
			return false;
		}
		$user->remove_cap( Agency_Pulse_Admin_Menu_Constants::CAP_MANAGE_MENUS );
		return true;
	}

	// -------------------------------------------------------------------------
	// Guard checks
	// -------------------------------------------------------------------------

	/**
	 * Return true when at least one user holds an explicit individual cap grant.
	 *
	 * Used to guard the UI: the Cap Only radio is disabled until this returns true.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function can_enable_cap_only(): bool {
		return count( self::get_explicit_holders() ) > 0;
	}
}
