<?php
/**
 * Global Shield demo mode runtime.
 *
 * @package Polanger_Shield
 */

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

class Polanger_Shield_Global_Demo {

  /**
   * Per-request throttle for block logging.
   *
   * Avoids repeated side effects without forcing database writes through
   * transients, which would conflict with strict database protection.
   *
   * @var array<string, bool>
   */
  private static $request_log_cache = array();

  /**
   * Cached normalized settings for the current request.
   *
   * @var array|null
   */
  private $settings_cache = null;

  /**
   * Cached enabled state for the current request.
   *
   * @var bool|null
   */
  private $enabled_cache = null;

  /**
   * Cached strict-mode state for the current request.
   *
   * @var bool|null
   */
  private $strict_mode_cache = null;

  /**
   * Cached targeted-user checks for the current request.
   *
   * @var array<int, bool>
   */
  private $targeted_user_cache = array();

  /**
   * Cached transport-token validation result for the current request.
   *
   * @var bool|null
   */
  private $transport_token_valid_cache = null;

  /**
   * Cached admin-referrer context result for the current request.
   *
   * @var bool|null
   */
  private $admin_referer_context_cache = null;

  /**
   * Cached REST-request detection result for the current request.
   *
   * @var bool|null
   */
  private $rest_request_cache = null;

  /**
   * Whether runtime hooks have already been attached for this request.
   *
   * @var bool
   */
  private $runtime_hooks_registered = false;

  /**
   * Option key.
   *
   * @var string
   */
  const OPTION_NAME = 'pdt_shield_global_demo_settings';

  /**
   * Query arg for blocked-request notices.
   *
   * @var string
   */
  const NOTICE_QUERY_ARG = 'polanger_shield_global_demo_notice';

  /**
   * Transport field keys.
   *
   * @var string
   */
  const REQUEST_MODE_KEY  = 'polanger_shield_global_demo_mode';
  const REQUEST_TOKEN_KEY = 'polanger_shield_global_demo_token';
  const HEADER_MODE_KEY   = 'HTTP_X_POLANGER_SHIELD_GLOBAL_DEMO';
  const HEADER_TOKEN_KEY  = 'HTTP_X_POLANGER_SHIELD_GLOBAL_DEMO_TOKEN';

  /**
   * Default settings.
   *
   * @var array
   */
  const DEFAULT_SETTINGS = array(
    'enabled'               => false,
    'target_user_ids'       => array(),
    'protection_level'      => 'balanced',
    'show_banner'           => true,
    'disable_controls'      => true,
    'block_post_requests'   => true,
    'block_ajax_requests'   => true,
    'block_rest_requests'   => true,
    'block_database_writes' => true,
    'block_emails'          => false,
    'block_http_requests'   => false,
    'block_media_uploads'   => false,
    'allowed_get_actions'   => array(),
    'allowed_ajax_actions'  => array( 'heartbeat' ),
    'allowed_rest_routes'   => array(),
  );

  /**
   * Singleton.
   *
   * @var Polanger_Shield_Global_Demo|null
   */
  private static $instance = null;

  /**
   * Get singleton.
   *
   * @return Polanger_Shield_Global_Demo
   */
  public static function instance() {
    if ( null === self::$instance ) {
      self::$instance = new self();
    }

    return self::$instance;
  }

  /**
   * Register runtime hooks.
   *
   * @return void
   */
  public function register_hooks() {
    add_action( 'init', array( $this, 'register_runtime_hooks' ), 0 );
  }

  /**
   * Register runtime hooks once WordPress pluggable functions are ready.
   *
   * @return void
   */
  public function register_runtime_hooks() {
    if ( $this->runtime_hooks_registered ) {
      return;
    }

    if ( ! function_exists( 'is_user_logged_in' ) || ! is_user_logged_in() || ! $this->is_enabled() || ! $this->is_current_user_targeted() ) {
      return;
    }

    if ( ! is_admin() && ! wp_doing_ajax() && ! $this->is_rest_request() ) {
      return;
    }

    $this->runtime_hooks_registered = true;

    $settings = $this->get_settings();

    add_action( 'admin_init', array( $this, 'maybe_block_unsafe_screen_load' ), 0 );
    add_action( 'admin_init', array( $this, 'maybe_block_dangerous_get_request' ), 0 );
    add_action( 'admin_init', array( $this, 'maybe_block_post_request' ), 0 );
    add_action( 'admin_init', array( $this, 'maybe_block_ajax_request' ), 0 );

    if ( ! empty( $settings['block_rest_requests'] ) ) {
      add_filter( 'rest_pre_dispatch', array( $this, 'maybe_block_rest_request' ), 5, 3 );
    }

    if ( 'strict' === $settings['protection_level'] && ! empty( $settings['block_database_writes'] ) ) {
      add_filter( 'query', array( $this, 'maybe_block_database_write_query' ), 5 );
    }

    if ( 'strict' === $settings['protection_level'] && ! empty( $settings['block_emails'] ) ) {
      add_filter( 'pre_wp_mail', array( $this, 'maybe_block_outgoing_mail' ), 5, 2 );
    }

    if ( 'strict' === $settings['protection_level'] && ! empty( $settings['block_http_requests'] ) ) {
      add_filter( 'pre_http_request', array( $this, 'maybe_block_outgoing_http' ), 5, 3 );
    }

    if ( 'strict' === $settings['protection_level'] && ! empty( $settings['block_media_uploads'] ) ) {
      add_filter( 'wp_handle_upload_prefilter', array( $this, 'maybe_block_upload' ), 5 );
    }

    add_action( 'admin_notices', array( $this, 'render_block_notice' ) );
    add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_runtime_assets' ), 115 );
  }

  /**
   * Return current settings.
   *
   * @return array
   */
  public function get_settings() {
    if ( null !== $this->settings_cache ) {
      return $this->settings_cache;
    }

    $settings = get_option( self::OPTION_NAME, array() );
    $settings = is_array( $settings ) ? $settings : array();
    $settings = wp_parse_args( $settings, self::DEFAULT_SETTINGS );

    $settings['enabled']               = ! empty( $settings['enabled'] );
    $settings['target_user_ids']       = $this->get_auto_target_user_ids();
    $settings['protection_level']      = in_array( $settings['protection_level'], array( 'balanced', 'strict' ), true ) ? $settings['protection_level'] : 'balanced';
    $settings['show_banner']           = ! empty( $settings['show_banner'] );
    $settings['disable_controls']      = ! empty( $settings['disable_controls'] );
    $settings['block_post_requests']   = ! empty( $settings['block_post_requests'] );
    $settings['block_ajax_requests']   = ! empty( $settings['block_ajax_requests'] );
    $settings['block_rest_requests']   = ! empty( $settings['block_rest_requests'] );
    $settings['block_database_writes'] = ! empty( $settings['block_database_writes'] );
    $settings['block_emails']          = ! empty( $settings['block_emails'] );
    $settings['block_http_requests']   = ! empty( $settings['block_http_requests'] );
    $settings['block_media_uploads']   = ! empty( $settings['block_media_uploads'] );
    $settings['allowed_get_actions']   = $this->sanitize_token_list( $settings['allowed_get_actions'], 'sanitize_key' );
    $settings['allowed_ajax_actions']  = $this->sanitize_token_list( $settings['allowed_ajax_actions'], 'sanitize_key' );
    $settings['allowed_rest_routes']   = $this->sanitize_token_list( $settings['allowed_rest_routes'], 'sanitize_text_field' );

    if ( empty( $settings['allowed_ajax_actions'] ) ) {
      $settings['allowed_ajax_actions'] = array( 'heartbeat' );
    }

    $this->settings_cache = $settings;

    return $this->settings_cache;
  }

  /**
   * Sanitize incoming settings.
   *
   * @param array $input Raw input.
   * @return array
   */
  public function sanitize_settings( $input ) {
    $input = is_array( $input ) ? $input : array();

    $settings = $this->get_settings();
    $settings['enabled']               = ! empty( $input['enabled'] );
    $settings['target_user_ids']       = $this->get_auto_target_user_ids();
    $settings['protection_level']      = isset( $input['protection_level'] ) && in_array( $input['protection_level'], array( 'balanced', 'strict' ), true ) ? $input['protection_level'] : 'balanced';
    $settings['show_banner']           = ! empty( $input['show_banner'] );
    $settings['disable_controls']      = ! empty( $input['disable_controls'] );
    $settings['block_post_requests']   = ! empty( $input['block_post_requests'] );
    $settings['block_ajax_requests']   = ! empty( $input['block_ajax_requests'] );
    $settings['block_rest_requests']   = ! empty( $input['block_rest_requests'] );
    $settings['block_database_writes'] = ! empty( $input['block_database_writes'] );
    $settings['block_emails']          = ! empty( $input['block_emails'] );
    $settings['block_http_requests']   = ! empty( $input['block_http_requests'] );
    $settings['block_media_uploads']   = ! empty( $input['block_media_uploads'] );
    $settings['allowed_get_actions']   = $this->sanitize_multiline_tokens( isset( $input['allowed_get_actions'] ) ? $input['allowed_get_actions'] : '', 'sanitize_key' );
    $settings['allowed_ajax_actions']  = $this->sanitize_multiline_tokens( isset( $input['allowed_ajax_actions'] ) ? $input['allowed_ajax_actions'] : '', 'sanitize_key' );
    $settings['allowed_rest_routes']   = $this->sanitize_multiline_tokens( isset( $input['allowed_rest_routes'] ) ? $input['allowed_rest_routes'] : '', 'sanitize_text_field' );

    if ( empty( $settings['allowed_ajax_actions'] ) ) {
      $settings['allowed_ajax_actions'] = array( 'heartbeat' );
    }

    return $settings;
  }

  /**
   * Persist settings.
   *
   * @param array $settings Sanitized settings.
   * @return bool
   */
  public function update_settings( $settings ) {
    $updated = update_option( self::OPTION_NAME, $settings );
    $this->flush_runtime_cache();

    return $updated;
  }

  /**
   * Return settings URL.
   *
   * @return string
   */
  public static function get_settings_url() {
    return admin_url( 'admin.php?page=polanger-settings&tab=shield#polanger-shield-global-demo' );
  }

  /**
   * Return readable level label.
   *
   * @param string $level Protection level.
   * @return string
   */
  public function get_level_label( $level ) {
    return 'strict' === sanitize_key( $level )
      ? __( 'Strict', 'polanger-admin-menu-manager' )
      : __( 'Balanced', 'polanger-admin-menu-manager' );
  }

  /**
   * Whether global demo mode is enabled with at least one affected user.
   *
   * @return bool
   */
  public function is_enabled() {
    if ( null !== $this->enabled_cache ) {
      return $this->enabled_cache;
    }

    $settings = $this->get_settings();

    if ( empty( $settings['enabled'] ) || empty( $settings['target_user_ids'] ) ) {
      $this->enabled_cache = false;
      return $this->enabled_cache;
    }

    foreach ( $settings['target_user_ids'] as $user_id ) {
      if ( $this->is_target_user_eligible( $user_id ) ) {
        $this->enabled_cache = true;
        return $this->enabled_cache;
      }
    }

    $this->enabled_cache = false;

    return $this->enabled_cache;
  }

  /**
   * Whether strict mode is active.
   *
   * @return bool
   */
  public function is_strict_mode() {
    if ( null !== $this->strict_mode_cache ) {
      return $this->strict_mode_cache;
    }

    $settings = $this->get_settings();

    $this->strict_mode_cache = $this->is_enabled() && 'strict' === $settings['protection_level'];

    return $this->strict_mode_cache;
  }

  /**
   * Whether the current user is affected by global demo mode.
   *
   * @return bool
   */
  public function is_current_user_targeted() {
    return $this->is_user_targeted( get_current_user_id() );
  }

  /**
   * Whether a user is affected by global demo mode.
   *
   * Shield managers and the protected admin always bypass the global lock so
   * the site never gets stranded without recovery access.
   *
   * @param int $user_id User ID.
   * @return bool
   */
  public function is_user_targeted( $user_id ) {
    $user_id = absint( $user_id );

    if ( isset( $this->targeted_user_cache[ $user_id ] ) ) {
      return $this->targeted_user_cache[ $user_id ];
    }

    if ( ! $user_id || ! $this->is_enabled() ) {
      $this->targeted_user_cache[ $user_id ] = false;
      return $this->targeted_user_cache[ $user_id ];
    }

    if ( Polanger_Shield_Safe_Mode::is_active() ) {
      $this->targeted_user_cache[ $user_id ] = false;
      return $this->targeted_user_cache[ $user_id ];
    }

    if ( $this->is_user_bypassed( $user_id ) ) {
      $this->targeted_user_cache[ $user_id ] = false;
      return $this->targeted_user_cache[ $user_id ];
    }

    $settings = $this->get_settings();
    $this->targeted_user_cache[ $user_id ] = in_array( $user_id, $settings['target_user_ids'], true );

    return $this->targeted_user_cache[ $user_id ];
  }

  /**
   * Render dashboard summary card.
   *
   * @return void
   */
  public function render_dashboard_summary() {
    if ( ! Polanger_Shield_Permissions::current_user_can_manage_shield( true ) ) {
      return;
    }

    if ( ! $this->is_enabled() ) {
      return;
    }

    $settings      = $this->get_settings();
    $target_users  = $this->get_target_users();
    $strict_active = 'strict' === $settings['protection_level'];
    ?>
    <div class="polanger-shield-global-demo-summary">
      <div class="polanger-shield-global-demo-summary__content">
        <span class="polanger-shield-global-demo-summary__eyebrow"><?php esc_html_e( 'Global Demo Mode', 'polanger-admin-menu-manager' ); ?></span>
        <h2><?php esc_html_e( 'Turn wp-admin into a controlled read-only demo environment.', 'polanger-admin-menu-manager' ); ?></h2>
        <p><?php esc_html_e( 'Selected administrators can explore admin pages and workflows while Shield blocks saving, publishing, destructive actions, and optional strict side effects such as database writes, emails, uploads, and external requests.', 'polanger-admin-menu-manager' ); ?></p>
        <div class="polanger-shield-global-demo-summary__meta">
          <span class="polanger-shield-global-demo-pill is-active">
            <?php esc_html_e( 'Enabled', 'polanger-admin-menu-manager' ); ?>
          </span>
          <span class="polanger-shield-global-demo-pill">
            <?php
            printf(
              /* translators: %s protection level label */
              esc_html__( 'Protection: %s', 'polanger-admin-menu-manager' ),
              esc_html( $this->get_level_label( $settings['protection_level'] ) )
            );
            ?>
          </span>
          <span class="polanger-shield-global-demo-pill">
            <?php
            printf(
              /* translators: %d number of affected users */
              esc_html__( 'Affected admins: %d', 'polanger-admin-menu-manager' ),
              absint( count( $target_users ) )
            );
            ?>
          </span>
          <?php if ( $strict_active ) : ?>
            <span class="polanger-shield-global-demo-pill is-warning"><?php esc_html_e( 'Strict guards active', 'polanger-admin-menu-manager' ); ?></span>
          <?php endif; ?>
        </div>
      </div>

      <div class="polanger-shield-global-demo-summary__actions">
        <a class="button button-primary" href="<?php echo esc_url( self::get_settings_url() ); ?>">
          <?php esc_html_e( 'Open Demo Settings', 'polanger-admin-menu-manager' ); ?>
        </a>
        <a class="button button-secondary" href="<?php echo esc_url( admin_url( 'admin.php?page=polanger-shield' ) ); ?>">
          <?php esc_html_e( 'Review Shield Rules', 'polanger-admin-menu-manager' ); ?>
        </a>
      </div>
    </div>
    <?php
  }

  /**
   * Enqueue runtime assets for affected users.
   *
   * @return void
   */
  public function enqueue_runtime_assets() {
    if ( ! is_admin() || ! $this->is_current_user_targeted() ) {
      return;
    }

    $settings = $this->get_settings();

    wp_enqueue_style(
      'polanger-shield-global-demo',
      POLANGER_SHIELD_URL . 'assets/css/shield-global-demo.css',
      array(),
      POLANGER_SHIELD_VERSION
    );

    wp_enqueue_script(
      'polanger-shield-global-demo-runtime',
      POLANGER_SHIELD_URL . 'assets/js/shield-global-demo-runtime.js',
      array( 'jquery' ),
      POLANGER_SHIELD_VERSION,
      true
    );

    wp_localize_script(
      'polanger-shield-global-demo-runtime',
      'polangerShieldGlobalDemoRuntime',
      array(
        'enabled'               => true,
        'showBanner'            => ! empty( $settings['show_banner'] ),
        'disableControls'       => ! empty( $settings['disable_controls'] ),
        'blockFormSubmissions'  => ! empty( $settings['block_post_requests'] ),
        'blockAjaxRequests'     => ! empty( $settings['block_ajax_requests'] ),
        'blockRestRequests'     => ! empty( $settings['block_rest_requests'] ),
        'allowedAjaxActions'    => array_values( (array) $settings['allowed_ajax_actions'] ),
        'allowedRestRoutes'     => array_values( (array) $settings['allowed_rest_routes'] ),
        'transport'             => array(
          'modeField'    => self::REQUEST_MODE_KEY,
          'tokenField'   => self::REQUEST_TOKEN_KEY,
          'modeHeader'   => 'X-Polanger-Shield-Global-Demo',
          'tokenHeader'  => 'X-Polanger-Shield-Global-Demo-Token',
          'modeValue'    => '1',
          'token'        => $this->create_transport_token( get_current_user_id() ),
          'lockedLabel'  => __( 'Read Only', 'polanger-admin-menu-manager' ),
        ),
        'i18n'                  => array(
          'bannerTitle'   => __( 'Global Demo Mode Active', 'polanger-admin-menu-manager' ),
          'bannerText'    => __( 'You can inspect this admin area, but Shield is blocking save actions and destructive changes for your account.', 'polanger-admin-menu-manager' ),
          'dismissLabel'  => __( 'Understood', 'polanger-admin-menu-manager' ),
          'actionBlocked' => __( 'Global Demo Mode is active. Changes are disabled for your account.', 'polanger-admin-menu-manager' ),
          'formBlocked'   => __( 'Form submission blocked by Global Demo Mode.', 'polanger-admin-menu-manager' ),
        ),
      )
    );
  }

  /**
   * Block admin screens that can mutate content as soon as they load.
   *
   * Some wp-admin creator screens insert auto-drafts or seed records during the
   * initial GET load. Those screens are unsafe for a read-only demo session and
   * must be redirected before WordPress reaches the write path.
   *
   * @return void
   */
  public function maybe_block_unsafe_screen_load() {
    if ( 'GET' !== $this->get_request_method() ) {
      return;
    }

    if ( ! $this->should_apply_balanced_guards() || $this->is_rest_request() || wp_doing_ajax() ) {
      return;
    }

    if ( ! $this->is_unsafe_screen_load_request() ) {
      return;
    }

    $this->log_block_event(
      'screen_load',
      array(
        'request_uri' => $this->current_request_uri(),
      )
    );

    wp_safe_redirect(
      add_query_arg(
        self::NOTICE_QUERY_ARG,
        'screen:' . $this->current_admin_page(),
        $this->get_fallback_redirect_url()
      )
    );
    exit;
  }

  /**
   * Block dangerous GET-based admin actions.
   *
   * @return void
   */
  public function maybe_block_dangerous_get_request() {
    if ( 'GET' !== $this->get_request_method() ) {
      return;
    }

    if ( ! $this->should_apply_balanced_guards() ) {
      return;
    }

    $action = $this->get_requested_action();
    if ( '' === $action || $this->is_allowed_get_action( $action ) || ! $this->is_dangerous_get_action( $action ) ) {
      return;
    }

    $this->log_block_event(
      'get',
      array(
        'admin_action' => $action,
        'request_uri'  => $this->current_request_uri(),
      )
    );

    wp_safe_redirect(
      add_query_arg(
        self::NOTICE_QUERY_ARG,
        'get:' . $action,
        $this->get_fallback_redirect_url()
      )
    );
    exit;
  }

  /**
   * Block classic admin POST submissions.
   *
   * @return void
   */
  public function maybe_block_post_request() {
    if ( 'POST' !== $this->get_request_method() || wp_doing_ajax() ) {
      return;
    }

    if ( ! $this->should_apply_balanced_guards() ) {
      return;
    }

    $settings = $this->get_settings();
    if ( empty( $settings['block_post_requests'] ) ) {
      return;
    }

    $this->log_block_event(
      'post',
      array(
        'request_uri' => $this->current_request_uri(),
      )
    );

    wp_safe_redirect(
      add_query_arg(
        self::NOTICE_QUERY_ARG,
        'post',
        $this->get_fallback_redirect_url()
      )
    );
    exit;
  }

  /**
   * Block mutating admin-ajax requests.
   *
   * @return void
   */
  public function maybe_block_ajax_request() {
    if ( ! wp_doing_ajax() ) {
      return;
    }

    $method = $this->get_request_method();
    if ( ! in_array( $method, array( 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ), true ) ) {
      return;
    }

    $settings = $this->get_settings();
    if ( empty( $settings['block_ajax_requests'] ) ) {
      return;
    }

    $transport_guards_active = $this->should_apply_transport_guards();
    $strict_backstop_active  = $this->should_apply_strict_transport_backstop( $method );

    if ( ! $transport_guards_active && ! $strict_backstop_active ) {
      return;
    }

    $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : '';
    if ( 'GET' === $method && $this->is_allowed_get_action( $action ) ) {
      return;
    }

    if ( 'GET' !== $method && $this->is_allowed_ajax_action( $action ) ) {
      return;
    }

    $is_mutating = 'GET' === $method
      ? ( '' !== $action && ( $this->is_dangerous_get_action( $action ) || $this->is_potentially_mutating_ajax_action( $action, false ) ) )
      : true;

    if ( ! $is_mutating ) {
      return;
    }

    $this->log_block_event(
      'ajax',
      array(
        'request_method' => $method,
        'ajax_action' => $action,
      )
    );

    wp_send_json_error(
      array(
        'message' => __( 'Global Demo Mode is active. AJAX changes are disabled for your account.', 'polanger-admin-menu-manager' ),
      ),
      403
    );
  }

  /**
   * Block REST write requests from admin demo sessions.
   *
   * @param mixed           $result  Existing result.
   * @param WP_REST_Server  $server  REST server.
   * @param WP_REST_Request $request Request object.
   * @return mixed
   */
  public function maybe_block_rest_request( $result, $server, $request ) {
    unset( $server );

    $settings = $this->get_settings();
    if ( empty( $settings['block_rest_requests'] ) ) {
      return $result;
    }

    $method = strtoupper( (string) $request->get_method() );
    if ( ! in_array( $method, array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true ) ) {
      return $result;
    }

    $transport_guards_active = $this->should_apply_transport_guards();
    $strict_backstop_active  = $this->should_apply_strict_transport_backstop( $method );

    if ( ! $transport_guards_active && ! $strict_backstop_active ) {
      return $result;
    }

    $route = sanitize_text_field( (string) $request->get_route() );
    if ( $this->is_allowed_rest_route( $route ) ) {
      return $result;
    }

    $this->log_block_event(
      'rest',
      array(
        'rest_method' => $method,
        'rest_route'  => $route,
      )
    );

    return new WP_Error(
      'polanger_shield_global_demo_blocked',
      __( 'Global Demo Mode is active. REST changes are disabled for your account.', 'polanger-admin-menu-manager' ),
      array(
        'status' => 403,
      )
    );
  }

  /**
   * Block strict-mode database write queries.
   *
   * @param string $query SQL query.
   * @return string|false
   */
  public function maybe_block_database_write_query( $query ) {
    if ( ! is_string( $query ) || '' === $query ) {
      return $query;
    }

    if ( ! $this->should_apply_strict_guards() ) {
      return $query;
    }

    $settings = $this->get_settings();
    if ( empty( $settings['block_database_writes'] ) ) {
      return $query;
    }

    $operation = $this->detect_write_sql_operation( $query );
    if ( '' === $operation ) {
      return $query;
    }

    $this->log_block_event(
      'database',
      array(
        'sql_operation' => $operation,
      )
    );

    return false;
  }

  /**
   * Block emails in strict mode.
   *
   * @param null|bool $pre_wp_mail Short-circuit value.
   * @param array     $atts        Mail arguments.
   * @return null|bool
   */
  public function maybe_block_outgoing_mail( $pre_wp_mail, $atts ) {
    if ( ! $this->should_apply_strict_guards() ) {
      return $pre_wp_mail;
    }

    $settings = $this->get_settings();
    if ( empty( $settings['block_emails'] ) ) {
      return $pre_wp_mail;
    }

    $recipients = isset( $atts['to'] ) ? $atts['to'] : '';

    $this->log_block_event(
      'mail',
      array(
        'recipients' => is_array( $recipients ) ? implode( ', ', array_map( 'sanitize_email', $recipients ) ) : sanitize_text_field( (string) $recipients ),
      )
    );

    return false;
  }

  /**
   * Block external HTTP requests in strict mode.
   *
   * @param mixed  $preempt Whether to preempt the request.
   * @param array  $parsed_args Parsed arguments.
   * @param string $url Request URL.
   * @return mixed
   */
  public function maybe_block_outgoing_http( $preempt, $parsed_args, $url ) {
    unset( $parsed_args );

    if ( ! $this->should_apply_strict_guards() ) {
      return $preempt;
    }

    $settings = $this->get_settings();
    if ( empty( $settings['block_http_requests'] ) ) {
      return $preempt;
    }

    $url       = esc_url_raw( (string) $url );
    $site_host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
    $url_host  = (string) wp_parse_url( $url, PHP_URL_HOST );

    if ( '' !== $site_host && '' !== $url_host && strtolower( $site_host ) === strtolower( $url_host ) ) {
      return $preempt;
    }

    $this->log_block_event(
      'http',
      array(
        'request_url' => $url,
      )
    );

    return new WP_Error(
      'polanger_shield_global_demo_http_blocked',
      __( 'Global Demo Mode is active. External HTTP requests are blocked for this admin session.', 'polanger-admin-menu-manager' )
    );
  }

  /**
   * Block media uploads in strict mode.
   *
   * @param array $file File payload.
   * @return array
   */
  public function maybe_block_upload( $file ) {
    if ( ! is_array( $file ) || ! $this->should_apply_strict_guards() ) {
      return $file;
    }

    $settings = $this->get_settings();
    if ( empty( $settings['block_media_uploads'] ) ) {
      return $file;
    }

    $this->log_block_event(
      'upload',
      array(
        'file_name' => isset( $file['name'] ) ? sanitize_file_name( $file['name'] ) : '',
      )
    );

    $file['error'] = __( 'Global Demo Mode is active. Media uploads are disabled for your account.', 'polanger-admin-menu-manager' );
    return $file;
  }

  /**
   * Render a notice after blocked classic requests.
   *
   * @return void
   */
  public function render_block_notice() {
    if ( ! is_admin() ) {
      return;
    }

    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $notice = isset( $_GET[ self::NOTICE_QUERY_ARG ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::NOTICE_QUERY_ARG ] ) ) : '';
    if ( '' === $notice ) {
      return;
    }

    $notice = rawurldecode( $notice );

    $message = __( 'Global Demo Mode prevented this change. You can explore the admin area, but write actions are disabled for your account.', 'polanger-admin-menu-manager' );
    if ( 0 === strpos( $notice, 'screen:' ) ) {
      $message = __( 'Global Demo Mode blocked this screen because opening it would create or change content automatically for your account.', 'polanger-admin-menu-manager' );
    }
    ?>
    <div class="notice notice-warning">
      <p><?php echo esc_html( $message ); ?></p>
    </div>
    <?php
  }

  /**
   * Return target users.
   *
   * @return array
   */
  public function get_target_users() {
    $settings = $this->get_settings();
    if ( empty( $settings['target_user_ids'] ) ) {
      return array();
    }

    $target_ids = array_values(
      array_filter(
        $settings['target_user_ids'],
        array( $this, 'is_target_user_eligible' )
      )
    );

    if ( empty( $target_ids ) ) {
      return array();
    }

    return get_users(
      array(
        'include' => $target_ids,
        'orderby' => 'display_name',
      )
    );
  }

  /**
   * Return automatic demo targets based on the Shield managers list.
   *
   * Global Demo Mode now affects every administrator who is not configured
   * above as a Shield manager and is not the protected recovery admin.
   *
   * @return array<int>
   */
  private function get_auto_target_user_ids() {
    $admins = get_users(
      array(
        'role__in' => array( 'administrator' ),
        'fields'   => 'ids',
      )
    );

    $target_user_ids = array();

    foreach ( $admins as $admin ) {
      $user_id = absint( $admin );

      if ( $this->is_target_user_eligible( $user_id ) ) {
        $target_user_ids[] = $user_id;
      }
    }

    return array_values( array_unique( $target_user_ids ) );
  }

  /**
   * Whether balanced guards apply to current request.
   *
   * @return bool
   */
  private function should_apply_balanced_guards() {
    return $this->is_current_user_targeted() && is_admin();
  }

  /**
   * Whether transport-aware guards apply to current request.
   *
   * @return bool
   */
  private function should_apply_transport_guards() {
    if ( ! $this->is_current_user_targeted() ) {
      return false;
    }

    if ( is_admin() && ! wp_doing_ajax() && ! $this->is_rest_request() ) {
      return true;
    }

    if ( wp_doing_ajax() || $this->is_rest_request() ) {
      return $this->has_valid_transport_token() || $this->has_admin_referer_context();
    }

    return false;
  }

  /**
   * Whether strict transport backstops should apply without transport hints.
   *
   * @param string $method Current request method.
   * @return bool
   */
  private function should_apply_strict_transport_backstop( $method = '' ) {
    if ( ! $this->is_strict_mode() || ! $this->is_current_user_targeted() ) {
      return false;
    }

    $method = '' !== $method ? strtoupper( (string) $method ) : $this->get_request_method();

    if ( wp_doing_ajax() ) {
      if ( in_array( $method, array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true ) ) {
        return true;
      }

      if ( 'GET' === $method ) {
        $action = $this->get_requested_action();
        return '' !== $action && ( $this->is_dangerous_get_action( $action ) || $this->is_potentially_mutating_ajax_action( $action, false ) );
      }
    }

    if ( $this->is_rest_request() ) {
      return in_array( $method, array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true );
    }

    return false;
  }

  /**
   * Whether strict side-effect guards apply.
   *
   * @return bool
   */
  private function should_apply_strict_guards() {
    if ( ! $this->is_strict_mode() || ! $this->is_current_user_targeted() ) {
      return false;
    }

    if ( is_admin() && ! wp_doing_ajax() && ! $this->is_rest_request() ) {
      return $this->is_likely_mutating_request();
    }

    if ( wp_doing_ajax() || $this->is_rest_request() ) {
      return $this->should_apply_strict_transport_backstop();
    }

    return false;
  }

  /**
   * Determine whether a user should always bypass global demo mode.
   *
   * @param int $user_id User ID.
   * @return bool
   */
  private function is_user_bypassed( $user_id ) {
    $user_id = absint( $user_id );
    if ( ! $user_id ) {
      return true;
    }

    $protected_admin_id = Polanger_Shield_Permissions::get_protected_admin_id();
    if ( $protected_admin_id && $protected_admin_id === $user_id ) {
      return true;
    }

    $settings = Polanger_Shield_Settings::instance()->get_settings();
    $manager_user_ids = isset( $settings['manager_user_ids'] ) ? (array) $settings['manager_user_ids'] : array();
    $manager_user_ids = Polanger_Shield_Permissions::normalize_user_ids( $manager_user_ids );

    return in_array( $user_id, $manager_user_ids, true );
  }

  /**
   * Determine whether a target user remains eligible for demo mode.
   *
   * @param int $user_id User ID.
   * @return bool
   */
  private function is_target_user_eligible( $user_id ) {
    $user_id = absint( $user_id );

    if ( ! $user_id || $this->is_user_bypassed( $user_id ) ) {
      return false;
    }

    return user_can( $user_id, 'manage_options' );
  }

  /**
   * Determine whether the current request is likely trying to change state.
   *
   * Strict mode uses this as a backstop so passive screen loads remain stable
   * while save-like requests still get database and side-effect protection.
   *
   * @return bool
   */
  private function is_likely_mutating_request() {
    $method = $this->get_request_method();

    if ( in_array( $method, array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true ) ) {
      return true;
    }

    if ( wp_doing_ajax() && 'GET' !== $method ) {
      return true;
    }

    if ( $this->is_rest_request() && ! in_array( $method, array( 'GET', 'HEAD', 'OPTIONS' ), true ) ) {
      return true;
    }

    $action = $this->get_requested_action();
    if ( wp_doing_ajax() && 'GET' === $method && '' !== $action && ( $this->is_dangerous_get_action( $action ) || $this->is_potentially_mutating_ajax_action( $action, false ) ) ) {
      return true;
    }

    if ( 'GET' === $method && '' !== $action && $this->is_dangerous_get_action( $action ) ) {
      return true;
    }

    return $this->has_valid_transport_token();
  }

  /**
   * Determine whether a GET action is allowed.
   *
   * @param string $action Requested action.
   * @return bool
   */
  private function is_allowed_get_action( $action ) {
    $action   = sanitize_key( $action );
    $settings = $this->get_settings();

    return in_array( $action, $settings['allowed_get_actions'], true );
  }

  /**
   * Determine whether an AJAX action is allowed.
   *
   * @param string $action AJAX action.
   * @return bool
   */
  private function is_allowed_ajax_action( $action ) {
    $action   = sanitize_key( $action );
    $settings = $this->get_settings();

    return in_array( $action, $settings['allowed_ajax_actions'], true );
  }

  /**
   * Determine whether a REST route fragment is allowed.
   *
   * @param string $route REST route.
   * @return bool
   */
  private function is_allowed_rest_route( $route ) {
    $route    = sanitize_text_field( (string) $route );
    $settings = $this->get_settings();

    foreach ( $settings['allowed_rest_routes'] as $allowed_fragment ) {
      $allowed_fragment = trim( (string) $allowed_fragment );

      if ( '' !== $allowed_fragment && false !== strpos( $route, $allowed_fragment ) ) {
        return true;
      }
    }

    return false;
  }

  /**
   * Return the requested admin action.
   *
   * @return string
   */
  private function get_requested_action() {
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : '';

    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    if ( ( '' === $action || '-1' === $action ) && isset( $_REQUEST['action2'] ) ) {
      // phpcs:ignore WordPress.Security.NonceVerification.Recommended
      $action = sanitize_key( wp_unslash( $_REQUEST['action2'] ) );
    }

    return $action;
  }

  /**
   * Determine whether a GET action looks destructive.
   *
   * @param string $action Action slug.
   * @return bool
   */
  private function is_dangerous_get_action( $action ) {
    $action = sanitize_key( $action );

    if ( '' === $action || '-1' === $action ) {
      return false;
    }

    $dangerous = array(
      'activate',
      'deactivate',
      'delete',
      'trash',
      'untrash',
      'remove',
      'install-plugin',
      'install-theme',
      'upgrade-plugin',
      'upgrade-theme',
      'switch-theme',
      'toggle-auto-update',
      'enable',
      'disable',
    );

    if ( in_array( $action, $dangerous, true ) ) {
      return true;
    }

    $tokens = array(
      'delete',
      'remove',
      'activate',
      'deactivate',
      'install',
      'upgrade',
      'update',
      'switch',
      'trash',
      'reset',
    );

    foreach ( $tokens as $token ) {
      if ( false !== strpos( $action, $token ) ) {
        return true;
      }
    }

    return false;
  }

  /**
   * Determine whether the current admin screen is unsafe for read-only demos.
   *
   * @return bool
   */
  private function is_unsafe_screen_load_request() {
    $current_page = $this->current_admin_page();
    if ( '' === $current_page ) {
      return false;
    }

    return in_array( $current_page, $this->get_unsafe_screen_pages(), true );
  }

  /**
   * Return admin pages that can write data on initial load.
   *
   * @return array
   */
  private function get_unsafe_screen_pages() {
    $pages = array(
      'post-new.php',
    );

    $pages = apply_filters( 'polanger_shield_global_demo_unsafe_admin_pages', $pages );

    if ( ! is_array( $pages ) ) {
      $pages = array();
    }

    $pages = array_map( 'sanitize_file_name', $pages );
    $pages = array_map( 'trim', $pages );

    return array_values( array_unique( array_filter( $pages ) ) );
  }

  /**
   * Return the current admin page file.
   *
   * @return string
   */
  private function current_admin_page() {
    global $pagenow;

    return is_string( $pagenow ) ? sanitize_file_name( $pagenow ) : '';
  }

  /**
   * Determine whether an AJAX action looks mutating.
   *
   * @param string $action AJAX action.
   * @param bool   $allow_empty Whether an empty action should be treated as risky.
   * @return bool
   */
  private function is_potentially_mutating_ajax_action( $action, $allow_empty = true ) {
    $action = sanitize_key( (string) $action );

    if ( '' === $action ) {
      return (bool) $allow_empty;
    }

    if ( in_array( $action, array( 'heartbeat' ), true ) ) {
      return false;
    }

    $tokens = array(
      'save',
      'update',
      'delete',
      'trash',
      'publish',
      'create',
      'edit',
      'insert',
      'remove',
      'activate',
      'deactivate',
      'duplicate',
      'import',
      'export',
      'install',
      'reset',
      'submit',
      'inline-save',
      'inline-save-tax',
    );

    foreach ( $tokens as $token ) {
      if ( false !== strpos( $action, $token ) ) {
        return true;
      }
    }

    return false;
  }

  /**
   * Detect a mutating SQL operation.
   *
   * @param string $query SQL query.
   * @return string
   */
  private function detect_write_sql_operation( $query ) {
    $query = preg_replace( '#/\*.*?\*/#s', ' ', (string) $query );
    $query = ltrim( (string) $query );
    $query = strtoupper( $query );

    $operations = array(
      'INSERT',
      'UPDATE',
      'DELETE',
      'REPLACE',
      'ALTER',
      'CREATE',
      'DROP',
      'TRUNCATE',
      'RENAME',
    );

    foreach ( $operations as $operation ) {
      if ( 0 === strpos( $query, $operation ) ) {
        return $operation;
      }
    }

    return '';
  }

  /**
   * Whether the current request is a REST request.
   *
   * @return bool
   */
  private function is_rest_request() {
    if ( null !== $this->rest_request_cache ) {
      return $this->rest_request_cache;
    }

    if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
      $this->rest_request_cache = true;
      return $this->rest_request_cache;
    }

    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    if ( isset( $_GET['rest_route'] ) || isset( $_REQUEST['rest_route'] ) ) {
      $this->rest_request_cache = true;
      return $this->rest_request_cache;
    }

    $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
    if ( '' === $request_uri ) {
      $this->rest_request_cache = false;
      return $this->rest_request_cache;
    }

    $request_path = wp_normalize_path( (string) wp_parse_url( $request_uri, PHP_URL_PATH ) );
    $rest_path    = wp_normalize_path( (string) wp_parse_url( rest_url(), PHP_URL_PATH ) );

    $this->rest_request_cache = '' !== $request_path && '' !== $rest_path && 0 === strpos( $request_path, $rest_path );

    return $this->rest_request_cache;
  }

  /**
   * Validate transport token from runtime-enhanced requests.
   *
   * @return bool
   */
  private function has_valid_transport_token() {
    if ( null !== $this->transport_token_valid_cache ) {
      return $this->transport_token_valid_cache;
    }

    $user_id = get_current_user_id();
    if ( ! $user_id ) {
      $this->transport_token_valid_cache = false;
      return $this->transport_token_valid_cache;
    }

    $mode_value = $this->get_transport_value( self::REQUEST_MODE_KEY, self::HEADER_MODE_KEY );
    $token      = $this->get_transport_value( self::REQUEST_TOKEN_KEY, self::HEADER_TOKEN_KEY );

    if ( '1' !== $mode_value || '' === $token ) {
      $this->transport_token_valid_cache = false;
      return $this->transport_token_valid_cache;
    }

    $this->transport_token_valid_cache = (bool) wp_verify_nonce( $token, $this->get_transport_action( $user_id ) );

    return $this->transport_token_valid_cache;
  }

  /**
   * Detect whether the current AJAX/REST request came from wp-admin.
   *
   * This gives the backend a reliable admin-side backstop even when a write
   * request is fired before the runtime wrappers had a chance to inject
   * transport headers.
   *
   * @return bool
   */
  private function has_admin_referer_context() {
    if ( null !== $this->admin_referer_context_cache ) {
      return $this->admin_referer_context_cache;
    }

    $referrer = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';
    if ( '' === $referrer ) {
      $this->admin_referer_context_cache = false;
      return $this->admin_referer_context_cache;
    }

    $referrer_host = (string) wp_parse_url( $referrer, PHP_URL_HOST );
    $admin_host    = (string) wp_parse_url( admin_url(), PHP_URL_HOST );

    if ( '' !== $admin_host && '' !== $referrer_host && strtolower( $admin_host ) !== strtolower( $referrer_host ) ) {
      $this->admin_referer_context_cache = false;
      return $this->admin_referer_context_cache;
    }

    $referrer_path = wp_normalize_path( (string) wp_parse_url( $referrer, PHP_URL_PATH ) );
    $admin_path    = wp_normalize_path( (string) wp_parse_url( admin_url(), PHP_URL_PATH ) );

    $this->admin_referer_context_cache = '' !== $referrer_path && '' !== $admin_path && 0 === strpos( $referrer_path, $admin_path );

    return $this->admin_referer_context_cache;
  }

  /**
   * Create transport token for one user session.
   *
   * @param int $user_id User ID.
   * @return string
   */
  private function create_transport_token( $user_id ) {
    $user_id = absint( $user_id );

    if ( ! $user_id ) {
      return '';
    }

    return wp_create_nonce( $this->get_transport_action( $user_id ) );
  }

  /**
   * Build transport action name.
   *
   * @param int $user_id User ID.
   * @return string
   */
  private function get_transport_action( $user_id ) {
    return 'polanger_shield_global_demo_transport_' . absint( $user_id );
  }

  /**
   * Read request/header transport value.
   *
   * @param string $request_key Request field key.
   * @param string $server_key  Server header key.
   * @return string
   */
  private function get_transport_value( $request_key, $server_key ) {
    if ( isset( $_REQUEST[ $request_key ] ) ) {
      return sanitize_text_field( wp_unslash( (string) $_REQUEST[ $request_key ] ) );
    }

    if ( isset( $_SERVER[ $server_key ] ) ) {
      return sanitize_text_field( wp_unslash( (string) $_SERVER[ $server_key ] ) );
    }

    return '';
  }

  /**
   * Return normalized request method.
   *
   * @return string
   */
  private function get_request_method() {
    return isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
  }

  /**
   * Return current request URI for logging.
   *
   * @return string
   */
  private function current_request_uri() {
    return isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
  }

  /**
   * Resolve redirect fallback after blocked classic requests.
   *
   * @return string
   */
  private function get_fallback_redirect_url() {
    $referrer = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';

    if ( '' !== $referrer ) {
      return wp_validate_redirect( remove_query_arg( self::NOTICE_QUERY_ARG, $referrer ), admin_url() );
    }

    return admin_url();
  }

  /**
   * Log a blocked event with short throttling.
   *
   * @param string $channel Channel slug.
   * @param array  $meta    Extra meta.
   * @return void
   */
  private function log_block_event( $channel, $meta = array() ) {
    $channel = sanitize_key( $channel );
    $user_id = get_current_user_id();

    if ( '' === $channel || ! $user_id ) {
      return;
    }

    $cache_key = md5( $channel . ':' . $user_id . ':' . maybe_serialize( $meta ) );
    if ( isset( self::$request_log_cache[ $cache_key ] ) ) {
      return;
    }

    self::$request_log_cache[ $cache_key ] = true;

    if ( $this->should_skip_persistent_logging() ) {
      return;
    }

    Polanger_Shield_Activity::log(
      'shield_global_demo_blocked',
      array(
        'object_type' => 'shield_global_demo',
        'object_name' => 'Global Demo Mode',
        'meta'        => array_merge(
          array(
            'channel'          => $channel,
            'protection_level' => $this->get_settings()['protection_level'],
          ),
          $meta
        ),
      )
    );
  }

  /**
   * Determine whether activity logging must avoid persistent writes.
   *
   * When strict mode is explicitly protecting against database writes, the
   * logging path itself must not touch transients or activity tables.
   *
   * @return bool
   */
  private function should_skip_persistent_logging() {
    $settings = $this->get_settings();

    return $this->is_strict_mode() && ! empty( $settings['block_database_writes'] );
  }

  /**
   * Sanitize a stored token list.
   *
   * @param array|string $values   Raw values.
   * @param callable     $callback Sanitizer callback.
   * @return array
   */
  private function sanitize_token_list( $values, $callback ) {
    $values = is_array( $values ) ? $values : array();
    $values = array_map( $callback, $values );
    $values = array_map( 'trim', $values );

    return array_values( array_unique( array_filter( $values ) ) );
  }

  /**
   * Sanitize textarea token lists.
   *
   * @param string|array $value    Raw value.
   * @param callable     $callback Sanitizer callback.
   * @return array
   */
  private function sanitize_multiline_tokens( $value, $callback ) {
    if ( is_array( $value ) ) {
      $parts = $value;
    } else {
      $parts = preg_split( '/[\r\n,]+/', (string) $value );
    }

    $parts = is_array( $parts ) ? array_map( $callback, $parts ) : array();
    $parts = array_map( 'trim', $parts );

    return array_values( array_unique( array_filter( $parts ) ) );
  }

  /**
   * Flush request-level caches after settings mutations.
   *
   * @return void
   */
  private function flush_runtime_cache() {
    $this->settings_cache                = null;
    $this->enabled_cache                 = null;
    $this->strict_mode_cache             = null;
    $this->targeted_user_cache           = array();
    $this->transport_token_valid_cache   = null;
    $this->admin_referer_context_cache   = null;
    $this->rest_request_cache            = null;
  }
}
