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

/**
 * Centralises all role/capability decisions.
 *
 * WHO CAN EDIT EXEMPTION STATUS:
 *   Roles selected in the "Who Can Manage Exemptions" setting, via the
 *   rankd_tx_manage capability. Default: Administrators, Shop Managers.
 *   Administrator is always included and cannot be removed.
 *
 * ROLE-BASED EXEMPTION:
 *   Roles selected in the settings page (rankd_tx_exempt_roles) are tax
 *   exempt automatically. Individual users can override the role rules
 *   from their profile: Always exempt, or Never exempt (hard block).
 *
 * EXTERNAL OVERRIDE:
 *   Any plugin or theme can hook 'rankd_tx_skip_user' to return true for a user,
 *   which removes them from this plugin's management entirely.
 *   Example use: wholesale plugin skips its own users.
 *
 *   add_filter( 'rankd_tx_skip_user', function( $skip, $user_id ) {
 *       if ( user_has_wholesale_exemption( $user_id ) ) return true;
 *       return $skip;
 *   }, 10, 2 );
 */
class Rankd_Tx_Capabilities {

	/** Custom capability gating all exemption management in wp-admin. */
	const CAP = 'rankd_tx_manage';

	public static function init() {
		// Enforce Administrator at SAVE time, not just read time — a fully
		// deselected multiselect is omitted from the POST entirely, and
		// WooCommerce would otherwise store '' (which the UI then happily
		// redisplays as "nobody selected").
		add_filter( 'woocommerce_admin_settings_sanitize_option_rankd_tx_editor_roles', array( __CLASS__, 'sanitize_editor_roles' ) );

		// Keep role capabilities in sync with the "Who Can Manage
		// Exemptions" setting. The settings-page action always fires after
		// a save of our tab — unlike update_option_*, which short-circuits
		// when the stored value is unchanged. The option hooks remain for
		// programmatic updates from outside the settings screen; the
		// bootstrap covers first load after updating to a build that
		// introduces the capability (and any site where roles were reset).
		add_action( 'woocommerce_update_options_rankd-tx-settings',  array( __CLASS__, 'sync_editor_caps' ), 20 );
		add_action( 'update_option_rankd_tx_editor_roles', array( __CLASS__, 'sync_editor_caps' ), 10, 0 );
		add_action( 'add_option_rankd_tx_editor_roles',    array( __CLASS__, 'sync_editor_caps' ), 10, 0 );
		self::maybe_bootstrap_cap();
	}

	/**
	 * Save-time sanitizer: always an array, Administrator always included.
	 * What gets stored is exactly what gets enforced, so the settings UI
	 * redisplays Administrator as selected even if someone deselects it.
	 *
	 * @param  mixed $value Raw submitted value ('' when fully deselected).
	 * @return string[]
	 */
	public static function sanitize_editor_roles( $value ) {
		$value = array_filter( array_map( 'sanitize_key', (array) $value ) );
		if ( ! in_array( 'administrator', $value, true ) ) {
			$value[] = 'administrator';
		}
		return array_values( $value );
	}

	// -------------------------------------------------------------------------
	// Editor roles → capability sync
	// -------------------------------------------------------------------------

	/**
	 * Roles allowed to manage exemption data (setting), with Administrator
	 * force-included so a site owner can never lock themselves out.
	 *
	 * @return string[]
	 */
	public static function get_editor_roles() {
		$saved = get_option( 'rankd_tx_editor_roles', null );

		// Option never saved: the shipped default. Option saved but empty
		// or malformed (e.g. '' from a pre-1.8.1 fully-deselected save):
		// the owner removed everyone they could — that means admin-only,
		// NOT a silent fall-back to the default pair.
		if ( null === $saved ) {
			return array( 'administrator', 'shop_manager' );
		}

		$saved = array_filter( array_map( 'sanitize_key', (array) $saved ) );
		if ( ! in_array( 'administrator', $saved, true ) ) {
			$saved[] = 'administrator';
		}
		return array_values( $saved );
	}

	/**
	 * Grants/revokes the custom capability so it matches the setting.
	 * Role caps persist in the DB, so this only needs to run when the
	 * setting changes (plus the one-time bootstrap).
	 */
	public static function sync_editor_caps() {
		$allowed  = self::get_editor_roles();
		$wp_roles = wp_roles(); // Not `global $wp_roles` — that global is only reliably populated once something has already called wp_roles() elsewhere in the request. On a lean bootstrap (wp-cron.php in particular skips most of what warms this up on a normal admin page load), the raw global can still be null here, and $wp_roles->roles on null is a fatal array_keys() error.
		foreach ( array_keys( $wp_roles->roles ) as $slug ) {
			$role = get_role( $slug );
			if ( ! $role ) {
				continue;
			}
			if ( in_array( $slug, $allowed, true ) ) {
				$role->add_cap( self::CAP );
			} elseif ( $role->has_cap( self::CAP ) ) {
				$role->remove_cap( self::CAP );
			}
		}
	}

	/** One-time bootstrap: if Administrator lacks the cap, sync now. */
	private static function maybe_bootstrap_cap() {
		$admin = get_role( 'administrator' );
		if ( $admin && ! $admin->has_cap( self::CAP ) ) {
			self::sync_editor_caps();
		}
	}

	// -------------------------------------------------------------------------
	// Scope check
	// -------------------------------------------------------------------------

	/**
	 * Returns true if this plugin should manage tax exemption for the user.
	 *
	 * Checks the rankd_tx_skip_user filter — external systems can opt users out.
	 * (Role exclusion was removed in 1.5.0: exemption is deliberate, so there
	 * is nothing to protect non-exempt users from. Per-user blocking is now
	 * handled by the "Never exempt" override instead.)
	 *
	 * @param  int|WP_User  $user
	 * @return bool
	 */
	public static function is_in_scope( $user ) {
		$user_id = $user instanceof WP_User ? $user->ID : (int) $user;
		if ( ! $user_id ) {
			return false;
		}

		// External override — wholesale or any other system can skip their users.
		return ! apply_filters( 'rankd_tx_skip_user', false, $user_id );
	}

	// -------------------------------------------------------------------------
	// Per-user override (tri-state)
	// -------------------------------------------------------------------------

	/**
	 * Returns the user's exemption override.
	 *
	 * @param  int $user_id
	 * @return string 'always' — exempt regardless of role rules.
	 *                'never'  — never exempted by this plugin (hard block,
	 *                           including role rules and Pro certificates).
	 *                ''       — no override; follow the role rules.
	 */
	public static function get_override( $user_id ) {
		$raw = get_user_meta( $user_id, RANKD_TX_META_EXEMPT, true );

		if ( 'never' === $raw ) {
			return 'never';
		}
		// 'always', plus legacy truthy values from the pre-1.5.0 checkbox
		// ('1', 1, 'yes') — treated as Always so nobody loses status on update.
		if ( 'always' === $raw || in_array( $raw, array( '1', 1, 'yes', true ), true ) ) {
			return 'always';
		}
		return '';
	}

	/**
	 * Stores the user's exemption override.
	 *
	 * @param int    $user_id
	 * @param string $override 'always', 'never', or '' to follow role rules.
	 */
	public static function set_override( $user_id, $override ) {
		if ( 'always' === $override || 'never' === $override ) {
			update_user_meta( $user_id, RANKD_TX_META_EXEMPT, $override );
		} else {
			delete_user_meta( $user_id, RANKD_TX_META_EXEMPT );
		}
	}

	// -------------------------------------------------------------------------
	// Role-based exemption
	// -------------------------------------------------------------------------

	/**
	 * Returns the list of roles that are tax exempt (settings page).
	 *
	 * @return string[]
	 */
	public static function get_exempt_roles() {
		$saved = get_option( 'rankd_tx_exempt_roles', array() );
		return is_array( $saved ) ? $saved : array();
	}

	/**
	 * Returns true when the user holds at least one tax-exempt role.
	 *
	 * @param  int $user_id
	 * @return bool
	 */
	public static function user_has_exempt_role( $user_id ) {
		$exempt_roles = self::get_exempt_roles();
		if ( empty( $exempt_roles ) ) {
			return false;
		}

		$user = get_userdata( $user_id );
		if ( ! $user instanceof WP_User ) {
			return false;
		}

		foreach ( (array) $user->roles as $role ) {
			if ( in_array( $role, $exempt_roles, true ) ) {
				return true;
			}
		}
		return false;
	}

	// -------------------------------------------------------------------------
	// Edit capability check
	// -------------------------------------------------------------------------

	/**
	 * Returns true if the currently logged-in user may edit tax exempt status.
	 * Governed by the "Who Can Manage Exemptions" setting via the custom
	 * rankd_tx_manage capability (default: Administrator, Shop Manager).
	 *
	 * @return bool
	 */
	public static function current_user_can_edit() {
		return current_user_can( self::CAP );
	}

	/**
	 * Returns true if the currently logged-in user may VIEW exemption data
	 * (review queue, statuses, profile exemption info) without necessarily
	 * being able to change any of it.
	 *
	 * View rides on WooCommerce's own manage_woocommerce capability (shop
	 * staff) rather than a second custom setting — anyone with manage
	 * rights automatically has view rights too, since CAP is only ever
	 * granted to a subset of manage_woocommerce holders.
	 *
	 * @return bool
	 */
	public static function current_user_can_view() {
		return current_user_can( self::CAP ) || current_user_can( 'manage_woocommerce' );
	}

	// -------------------------------------------------------------------------
	// Settings helpers
	// -------------------------------------------------------------------------

	/**
	 * Returns all registered WordPress roles as an id => label array,
	 * for use in the settings page exempt-roles checklist.
	 *
	 * @return string[]
	 */
	public static function get_all_roles() {
		$wp_roles = wp_roles(); // See sync_editor_caps() above — always go through wp_roles(), never the raw global.
		$roles    = array();
		foreach ( $wp_roles->roles as $slug => $data ) {
			$roles[ $slug ] = $data['name'];
		}
		return $roles;
	}
}
