<?php
defined( 'ABSPATH' ) || exit;

/**
 * Who may read this plugin's screens, and who may change them.
 *
 * **The problem this solves.** Everything here used to be gated on
 * `manage_options`, which is not "can see lead reports" — it is every Settings
 * screen on the site, and the capability most plugins treat as "is this an
 * administrator". Somebody whose job is to read enquiries had to be handed the
 * keys to the whole site to do it, so sites were shimming around it: lending
 * `manage_options` for the duration of one screen's render. That works and it is
 * read-only by accident rather than by design.
 *
 * **Two capabilities, because the split is the point.** Reading who enquired and
 * changing what is collected about them are different jobs with different
 * consequences — turning on cross-domain handoff, widening the datapoint set or
 * shortening retention all have privacy consequences, and none of them is a
 * decision the person reading the reports should be making by accident.
 *
 *   bw_lead_ai_view    journeys, reports, the attribution screens. Read only.
 *   bw_lead_ai_manage  settings, handoff configuration, retention, imports,
 *                      resets — anything that writes.
 *
 * **`manage_options` remains a floor, and that widens nothing.** Anyone holding
 * it can already do all of this today, so granting them both capabilities changes
 * no one's access; what it buys is that **no existing site can lose a screen on
 * upgrade**. A role somebody built with `manage_options` — an agency "Manager",
 * whatever a membership plugin created — keeps working without anybody having to
 * notice this release happened. That matters more than tidiness here: the failure
 * mode of getting it wrong is a client locked out of their own reports.
 *
 * The floor is a `user_has_cap` filter rather than a stored role grant because it
 * needs no activation and no migration. Plugins update in place far more often
 * than they are activated, so anything that depends on an activation hook running
 * is a thing that silently does not happen on most sites.
 *
 * The role grant below is therefore belt-and-braces, and exists for a different
 * reason: capability-manager plugins list the capabilities that some role
 * actually holds. Without the grant, the two names this exists to offer would not
 * appear in the UI where somebody goes to assign them.
 */
class BW_Lead_AI_Caps {

	const VIEW   = 'bw_lead_ai_view';
	const MANAGE = 'bw_lead_ai_manage';

	/** Bumped if the capability set changes, so the role grant re-runs. */
	const GRANT_VERSION = 1;
	const GRANT_OPTION  = 'bw_lead_ai_caps_granted';

	private static $instance = null;

	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	public function register() {
		add_filter( 'user_has_cap', array( $this, 'administrator_floor' ), 10, 1 );
		add_action( 'admin_init', array( $this, 'maybe_grant_to_administrator' ) );
	}

	/**
	 * The capability required to READ. Filterable, so a site can raise it.
	 *
	 * Lowering it below what an administrator holds is possible and is the point —
	 * that is how the admissions-officer case is served.
	 */
	public static function view() {
		return (string) apply_filters( 'bw_lead_ai_view_capability', self::VIEW );
	}

	/** The capability required to CHANGE anything. */
	public static function manage() {
		return (string) apply_filters( 'bw_lead_ai_manage_capability', self::MANAGE );
	}

	public static function can_view() {
		return current_user_can( self::view() );
	}

	public static function can_manage() {
		return current_user_can( self::manage() );
	}

	/**
	 * Anyone who can `manage_options` can do everything here.
	 *
	 * Not a convenience: it is what makes this release invisible to every site
	 * that upgrades into it. See the class docblock — it grants nothing that was
	 * not already granted, because a `manage_options` holder could reach all of
	 * these screens before the capabilities existed.
	 *
	 * Only the shipped names are floored. A site that has FILTERED view() or
	 * manage() to something of its own has made a deliberate decision about who
	 * gets in, and quietly handing that to every administrator would undo it.
	 */
	public function administrator_floor( $allcaps ) {
		if ( ! empty( $allcaps['manage_options'] ) ) {
			$allcaps[ self::VIEW ]   = true;
			$allcaps[ self::MANAGE ] = true;
		}
		return $allcaps;
	}

	/**
	 * Put the capabilities on the administrator role, once.
	 *
	 * Stamped rather than checked against the role, so this is one option read on
	 * an admin request and not a role write on every one. `add_cap()` persists to
	 * the database, so it must not run per request.
	 */
	public function maybe_grant_to_administrator() {
		if ( (int) get_option( self::GRANT_OPTION ) === self::GRANT_VERSION ) {
			return;
		}
		$role = get_role( 'administrator' );
		if ( $role ) {
			$role->add_cap( self::VIEW );
			$role->add_cap( self::MANAGE );
		}
		update_option( self::GRANT_OPTION, self::GRANT_VERSION, false );
	}

	/** Called from uninstall.php. Leaves no capability behind on any role. */
	public static function remove_all() {
		foreach ( wp_roles()->get_names() as $slug => $unused ) {
			$role = get_role( $slug );
			if ( ! $role ) {
				continue;
			}
			$role->remove_cap( self::VIEW );
			$role->remove_cap( self::MANAGE );
		}
	}
}
