<?php
/**
 * Settings service for Polanger Firewall.
 *
 * @package Polanger_Dashboard_Tools
 */

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

/**
 * Stores and sanitizes firewall configuration.
 */
class PDT_Firewall_Settings {

  /**
   * Option key.
   */
  const OPTION_NAME = 'pdt_firewall_settings';

  /**
   * Return default settings.
   *
   * @return array
   */
  public function get_defaults() {
    return array(
      'enabled'                       => false,
      'mode'                          => 'monitor',
      'admin_bypass'                  => true,
      'log_events'                    => true,
      'event_retention'               => 'monthly',
      'auth_rate_limit_enabled'       => true,
      'login_max_attempts'            => 5,
      'login_ip_max_attempts'         => 15,
      'lostpassword_max_attempts'     => 3,
      'register_max_attempts'         => 3,
      'auth_window_minutes'           => 10,
      'xmlrpc_protection_enabled'     => true,
      'xmlrpc_disable_auth'           => true,
      'xmlrpc_disable_multicall'      => true,
      'xmlrpc_disable_pingbacks'      => false,
      'rest_protection_enabled'       => true,
      'rest_rate_limit_enabled'       => true,
      'rest_rate_limit'               => 120,
      'rest_rate_window_minutes'      => 10,
      'rest_block_anonymous_writes'   => false,
      'rest_block_user_enumeration'   => true,
      'rest_write_route_allowlist'     => "/wc/store/\n/contact-form-7/",
      'security_headers_enabled'       => false,
      'security_headers_nosniff'       => true,
      'security_headers_frame_options' => true,
      'security_headers_referrer'      => true,
      'security_headers_permissions'   => true,
      'security_headers_csp_frame'     => false,
      'app_passwords_guard_enabled'    => true,
      'app_passwords_log_usage'        => true,
      'app_passwords_admin_only'       => false,
      'user_enumeration_guard_enabled' => true,
      'user_enumeration_author_query'  => true,
      'request_scoring_enabled'        => true,
      'request_score_threshold'        => 5,
      'request_score_temp_threshold'   => 8,
      'probe_protection_enabled'      => true,
      'temp_block_enabled'            => true,
      'probe_strikes'                 => 3,
      'probe_window_minutes'          => 10,
      'temp_block_minutes'            => 30,
      'max_url_length'                => 2048,
      'max_query_length'              => 1500,
      'allowlist_ips'                 => '',
      'denylist_ips'                  => '',
      'trusted_proxy_headers_enabled' => false,
      'proxy_ip_header'               => 'none',
      'trusted_proxy_ips'             => '',
    );
  }

  /**
   * Return sanitized persisted settings.
   *
   * @return array
   */
  public function get_settings() {
    $settings = get_option( self::OPTION_NAME, array() );
    $settings = is_array( $settings ) ? $settings : array();

    return $this->sanitize_settings( wp_parse_args( $settings, $this->get_defaults() ) );
  }

  /**
   * Save settings payload.
   *
   * @param array $raw     Raw submitted values.
   * @param array $touched Setting keys explicitly changed in the UI.
   * @return array
   */
  public function save_settings( $raw, $touched = array() ) {
    $raw      = is_array( $raw ) ? $raw : array();
    $touched  = is_array( $touched ) ? array_map( 'sanitize_key', $touched ) : array();
    $touched  = array_values( array_unique( $touched ) );
    $previous = $this->get_settings();

    if ( ! array_key_exists( 'event_retention', $raw ) ) {
      $raw['event_retention'] = isset( $previous['event_retention'] ) ? $previous['event_retention'] : $this->get_defaults()['event_retention'];
    }

    foreach ( $this->get_boolean_keys() as $key ) {
      $raw[ $key ] = ! empty( $raw[ $key ] );
    }

    $raw      = $this->apply_mode_profile_to_raw( $raw, $previous, $touched );
    $settings = $this->sanitize_settings( $raw );
    update_option( self::OPTION_NAME, $settings, false );

    return $settings;
  }

  /**
   * Save only the event retention setting.
   *
   * @param mixed $retention Submitted retention key.
   * @return array
   */
  public function save_event_retention( $retention ) {
    $settings                    = $this->get_settings();
    $settings['event_retention'] = $this->sanitize_event_retention( $retention );

    update_option( self::OPTION_NAME, $settings, false );

    return $settings;
  }

  /**
   * Return event retention choices.
   *
   * @return array
   */
  public function get_event_retention_choices() {
    return array(
      'daily'   => array(
        'label'       => __( 'Daily', 'polanger-admin-menu-manager' ),
        'description' => __( 'Keep only the last 24 hours of firewall events.', 'polanger-admin-menu-manager' ),
      ),
      'weekly'  => array(
        'label'       => __( 'Weekly', 'polanger-admin-menu-manager' ),
        'description' => __( 'Keep firewall events from the last 7 days.', 'polanger-admin-menu-manager' ),
      ),
      'monthly' => array(
        'label'       => __( 'Monthly', 'polanger-admin-menu-manager' ),
        'description' => __( 'Keep firewall events from the last 30 days.', 'polanger-admin-menu-manager' ),
      ),
      'yearly'  => array(
        'label'       => __( 'Yearly', 'polanger-admin-menu-manager' ),
        'description' => __( 'Keep firewall events from the last 365 days.', 'polanger-admin-menu-manager' ),
      ),
    );
  }

  /**
   * Get firewall mode definitions.
   *
   * @return array
   */
  public function get_modes() {
    return array(
      'monitor'  => array(
        'label'       => __( 'Monitor Only', 'polanger-admin-menu-manager' ),
        'description' => __( 'Logs suspicious requests without blocking them. Use this first on sensitive sites.', 'polanger-admin-menu-manager' ),
      ),
      'balanced' => array(
        'label'       => __( 'Balanced', 'polanger-admin-menu-manager' ),
        'description' => __( 'Recommended default. Blocks obvious probes and protects auth, XML-RPC, REST rate pressure, and anonymous user enumeration with low lockout risk.', 'polanger-admin-menu-manager' ),
      ),
      'strict'   => array(
        'label'       => __( 'Strict', 'polanger-admin-menu-manager' ),
        'description' => __( 'More aggressive protection for sites under attack. Review allowlists before enabling on production sites.', 'polanger-admin-menu-manager' ),
      ),
    );
  }

  /**
   * Return preset defaults for each firewall mode.
   *
   * These profiles are applied only when the selected mode changes. If the
   * submitted value differs from the previously saved value, it is treated as
   * an intentional user override and is preserved.
   *
   * @return array
   */
  public function get_mode_profiles() {
    return array(
      'monitor'  => array(
        'admin_bypass'                  => true,
        'log_events'                    => true,
        'auth_rate_limit_enabled'       => true,
        'login_max_attempts'            => 5,
        'login_ip_max_attempts'         => 15,
        'lostpassword_max_attempts'     => 3,
        'register_max_attempts'         => 3,
        'auth_window_minutes'           => 10,
        'xmlrpc_protection_enabled'     => true,
        'xmlrpc_disable_auth'           => true,
        'xmlrpc_disable_multicall'      => true,
        'xmlrpc_disable_pingbacks'      => false,
        'rest_protection_enabled'       => true,
        'rest_rate_limit_enabled'       => true,
        'rest_rate_limit'               => 120,
        'rest_rate_window_minutes'      => 10,
        'rest_block_anonymous_writes'   => false,
        'rest_block_user_enumeration'   => true,
        'security_headers_enabled'       => false,
        'security_headers_nosniff'       => true,
        'security_headers_frame_options' => true,
        'security_headers_referrer'      => true,
        'security_headers_permissions'   => true,
        'security_headers_csp_frame'     => false,
        'app_passwords_guard_enabled'    => true,
        'app_passwords_log_usage'        => true,
        'app_passwords_admin_only'       => false,
        'user_enumeration_guard_enabled' => true,
        'user_enumeration_author_query'  => true,
        'request_scoring_enabled'        => true,
        'request_score_threshold'        => 5,
        'request_score_temp_threshold'   => 8,
        'probe_protection_enabled'      => true,
        'temp_block_enabled'            => true,
        'probe_strikes'                 => 3,
        'probe_window_minutes'          => 10,
        'temp_block_minutes'            => 30,
        'max_url_length'                => 2048,
        'max_query_length'              => 1500,
      ),
      'balanced' => array(
        'admin_bypass'                  => true,
        'log_events'                    => true,
        'auth_rate_limit_enabled'       => true,
        'login_max_attempts'            => 5,
        'login_ip_max_attempts'         => 15,
        'lostpassword_max_attempts'     => 3,
        'register_max_attempts'         => 3,
        'auth_window_minutes'           => 10,
        'xmlrpc_protection_enabled'     => true,
        'xmlrpc_disable_auth'           => true,
        'xmlrpc_disable_multicall'      => true,
        'xmlrpc_disable_pingbacks'      => false,
        'rest_protection_enabled'       => true,
        'rest_rate_limit_enabled'       => true,
        'rest_rate_limit'               => 120,
        'rest_rate_window_minutes'      => 10,
        'rest_block_anonymous_writes'   => false,
        'rest_block_user_enumeration'   => true,
        'security_headers_enabled'       => true,
        'security_headers_nosniff'       => true,
        'security_headers_frame_options' => true,
        'security_headers_referrer'      => true,
        'security_headers_permissions'   => true,
        'security_headers_csp_frame'     => false,
        'app_passwords_guard_enabled'    => true,
        'app_passwords_log_usage'        => true,
        'app_passwords_admin_only'       => false,
        'user_enumeration_guard_enabled' => true,
        'user_enumeration_author_query'  => true,
        'request_scoring_enabled'        => true,
        'request_score_threshold'        => 5,
        'request_score_temp_threshold'   => 8,
        'probe_protection_enabled'      => true,
        'temp_block_enabled'            => true,
        'probe_strikes'                 => 3,
        'probe_window_minutes'          => 10,
        'temp_block_minutes'            => 30,
        'max_url_length'                => 2048,
        'max_query_length'              => 1500,
      ),
      'strict'   => array(
        'admin_bypass'                  => true,
        'log_events'                    => true,
        'auth_rate_limit_enabled'       => true,
        'login_max_attempts'            => 3,
        'login_ip_max_attempts'         => 8,
        'lostpassword_max_attempts'     => 2,
        'register_max_attempts'         => 2,
        'auth_window_minutes'           => 15,
        'xmlrpc_protection_enabled'     => true,
        'xmlrpc_disable_auth'           => true,
        'xmlrpc_disable_multicall'      => true,
        'xmlrpc_disable_pingbacks'      => true,
        'rest_protection_enabled'       => true,
        'rest_rate_limit_enabled'       => true,
        'rest_rate_limit'               => 60,
        'rest_rate_window_minutes'      => 10,
        'rest_block_anonymous_writes'   => true,
        'rest_block_user_enumeration'   => true,
        'security_headers_enabled'       => true,
        'security_headers_nosniff'       => true,
        'security_headers_frame_options' => true,
        'security_headers_referrer'      => true,
        'security_headers_permissions'   => true,
        'security_headers_csp_frame'     => true,
        'app_passwords_guard_enabled'    => true,
        'app_passwords_log_usage'        => true,
        'app_passwords_admin_only'       => true,
        'user_enumeration_guard_enabled' => true,
        'user_enumeration_author_query'  => true,
        'request_scoring_enabled'        => true,
        'request_score_threshold'        => 4,
        'request_score_temp_threshold'   => 6,
        'probe_protection_enabled'      => true,
        'temp_block_enabled'            => true,
        'probe_strikes'                 => 1,
        'probe_window_minutes'          => 15,
        'temp_block_minutes'            => 60,
        'max_url_length'                => 1536,
        'max_query_length'              => 1024,
      ),
    );
  }

  /**
   * Return preset defaults for one firewall mode.
   *
   * @param string $mode Mode key.
   * @return array
   */
  public function get_mode_profile( $mode ) {
    $profiles = $this->get_mode_profiles();
    $mode     = sanitize_key( $mode );

    return isset( $profiles[ $mode ] ) ? $profiles[ $mode ] : $profiles['balanced'];
  }

  /**
   * Whether firewall runtime is enabled.
   *
   * @return bool
   */
  public function is_enabled() {
    $settings = $this->get_settings();

    return ! empty( $settings['enabled'] );
  }

  /**
   * Whether active mode is monitor only.
   *
   * @return bool
   */
  public function is_monitor_mode() {
    $settings = $this->get_settings();

    return 'monitor' === $settings['mode'];
  }

  /**
   * Return a scalar setting.
   *
   * @param string $key     Setting key.
   * @param mixed  $default Default value.
   * @return mixed
   */
  public function get( $key, $default = null ) {
    $settings = $this->get_settings();

    return array_key_exists( $key, $settings ) ? $settings[ $key ] : $default;
  }

  /**
   * Return one auth limit profile.
   *
   * @param string $action Action key.
   * @return array
   */
  public function get_auth_limit_profile( $action ) {
    $settings = $this->get_settings();
    $window   = max( 1, absint( $settings['auth_window_minutes'] ) );

    switch ( sanitize_key( $action ) ) {
      case 'lostpassword':
        return array(
          'max_attempts'    => max( 1, absint( $settings['lostpassword_max_attempts'] ) ),
          'ip_max_attempts' => max( 1, absint( $settings['login_ip_max_attempts'] ) ),
          'window_minutes'  => $window,
        );
      case 'register':
        return array(
          'max_attempts'    => max( 1, absint( $settings['register_max_attempts'] ) ),
          'ip_max_attempts' => max( 1, absint( $settings['login_ip_max_attempts'] ) ),
          'window_minutes'  => $window,
        );
      case 'login':
      default:
        return array(
          'max_attempts'    => max( 1, absint( $settings['login_max_attempts'] ) ),
          'ip_max_attempts' => max( 1, absint( $settings['login_ip_max_attempts'] ) ),
          'window_minutes'  => $window,
        );
    }
  }

  /**
   * Get cleaned IP list lines.
   *
   * @param string $key Settings key.
   * @return array
   */
  public function get_ip_list( $key ) {
    $settings = $this->get_settings();
    $raw      = isset( $settings[ $key ] ) ? $settings[ $key ] : '';

    return $this->sanitize_ip_list_to_array( $raw );
  }

  /**
   * Get cleaned path prefix list lines.
   *
   * @param string $key Settings key.
   * @return array
   */
  public function get_path_prefix_list( $key ) {
    $settings = $this->get_settings();
    $raw      = isset( $settings[ $key ] ) ? $settings[ $key ] : '';

    return $this->sanitize_path_prefix_list_to_array( $raw );
  }

  /**
   * Sanitize settings.
   *
   * @param array $raw Raw settings.
   * @return array
   */
  private function sanitize_settings( $raw ) {
    $defaults = $this->get_defaults();
    $raw      = wp_parse_args( is_array( $raw ) ? $raw : array(), $defaults );
    $modes    = $this->get_modes();

    $settings = array();

    $settings['enabled']                     = ! empty( $raw['enabled'] );
    $settings['mode']                        = isset( $modes[ sanitize_key( $raw['mode'] ) ] ) ? sanitize_key( $raw['mode'] ) : $defaults['mode'];
    $settings['admin_bypass']                = ! empty( $raw['admin_bypass'] );
    $settings['log_events']                  = ! empty( $raw['log_events'] );
    $settings['event_retention']             = $this->sanitize_event_retention( $raw['event_retention'] );
    $settings['auth_rate_limit_enabled']     = ! empty( $raw['auth_rate_limit_enabled'] );
    $settings['login_max_attempts']          = $this->sanitize_int( $raw['login_max_attempts'], 1, 50, $defaults['login_max_attempts'] );
    $settings['login_ip_max_attempts']       = $this->sanitize_int( $raw['login_ip_max_attempts'], 1, 200, $defaults['login_ip_max_attempts'] );
    $settings['lostpassword_max_attempts']   = $this->sanitize_int( $raw['lostpassword_max_attempts'], 1, 50, $defaults['lostpassword_max_attempts'] );
    $settings['register_max_attempts']       = $this->sanitize_int( $raw['register_max_attempts'], 1, 50, $defaults['register_max_attempts'] );
    $settings['auth_window_minutes']         = $this->sanitize_int( $raw['auth_window_minutes'], 1, 1440, $defaults['auth_window_minutes'] );
    $settings['xmlrpc_protection_enabled']   = ! empty( $raw['xmlrpc_protection_enabled'] );
    $settings['xmlrpc_disable_auth']         = ! empty( $raw['xmlrpc_disable_auth'] );
    $settings['xmlrpc_disable_multicall']    = ! empty( $raw['xmlrpc_disable_multicall'] );
    $settings['xmlrpc_disable_pingbacks']    = ! empty( $raw['xmlrpc_disable_pingbacks'] );
    $settings['rest_protection_enabled']     = ! empty( $raw['rest_protection_enabled'] );
    $settings['rest_rate_limit_enabled']     = ! empty( $raw['rest_rate_limit_enabled'] );
    $settings['rest_rate_limit']             = $this->sanitize_int( $raw['rest_rate_limit'], 10, 2000, $defaults['rest_rate_limit'] );
    $settings['rest_rate_window_minutes']    = $this->sanitize_int( $raw['rest_rate_window_minutes'], 1, 1440, $defaults['rest_rate_window_minutes'] );
    $settings['rest_block_anonymous_writes'] = ! empty( $raw['rest_block_anonymous_writes'] );
    $settings['rest_block_user_enumeration'] = ! empty( $raw['rest_block_user_enumeration'] );
    $settings['rest_write_route_allowlist']   = $this->sanitize_path_prefix_list_text( $raw['rest_write_route_allowlist'] );
    $settings['security_headers_enabled']       = ! empty( $raw['security_headers_enabled'] );
    $settings['security_headers_nosniff']       = ! empty( $raw['security_headers_nosniff'] );
    $settings['security_headers_frame_options'] = ! empty( $raw['security_headers_frame_options'] );
    $settings['security_headers_referrer']      = ! empty( $raw['security_headers_referrer'] );
    $settings['security_headers_permissions']   = ! empty( $raw['security_headers_permissions'] );
    $settings['security_headers_csp_frame']     = ! empty( $raw['security_headers_csp_frame'] );
    $settings['app_passwords_guard_enabled']    = ! empty( $raw['app_passwords_guard_enabled'] );
    $settings['app_passwords_log_usage']        = ! empty( $raw['app_passwords_log_usage'] );
    $settings['app_passwords_admin_only']       = ! empty( $raw['app_passwords_admin_only'] );
    $settings['user_enumeration_guard_enabled'] = ! empty( $raw['user_enumeration_guard_enabled'] );
    $settings['user_enumeration_author_query']  = ! empty( $raw['user_enumeration_author_query'] );
    $settings['request_scoring_enabled']        = ! empty( $raw['request_scoring_enabled'] );
    $settings['request_score_threshold']        = $this->sanitize_int( $raw['request_score_threshold'], 1, 20, $defaults['request_score_threshold'] );
    $settings['request_score_temp_threshold']   = $this->sanitize_int( $raw['request_score_temp_threshold'], 1, 30, $defaults['request_score_temp_threshold'] );
    $settings['probe_protection_enabled']    = ! empty( $raw['probe_protection_enabled'] );
    $settings['temp_block_enabled']          = ! empty( $raw['temp_block_enabled'] );
    $settings['probe_strikes']               = $this->sanitize_int( $raw['probe_strikes'], 1, 50, $defaults['probe_strikes'] );
    $settings['probe_window_minutes']        = $this->sanitize_int( $raw['probe_window_minutes'], 1, 1440, $defaults['probe_window_minutes'] );
    $settings['temp_block_minutes']          = $this->sanitize_int( $raw['temp_block_minutes'], 1, 1440, $defaults['temp_block_minutes'] );
    $settings['max_url_length']              = $this->sanitize_int( $raw['max_url_length'], 256, 10000, $defaults['max_url_length'] );
    $settings['max_query_length']            = $this->sanitize_int( $raw['max_query_length'], 128, 10000, $defaults['max_query_length'] );
    $settings['allowlist_ips']               = $this->sanitize_ip_list_text( $raw['allowlist_ips'] );
    $settings['denylist_ips']                = $this->sanitize_ip_list_text( $raw['denylist_ips'] );
    $settings['trusted_proxy_headers_enabled'] = ! empty( $raw['trusted_proxy_headers_enabled'] );
    $settings['proxy_ip_header']               = $this->sanitize_proxy_ip_header( $raw['proxy_ip_header'] );
    $settings['trusted_proxy_ips']             = $this->sanitize_ip_list_text( $raw['trusted_proxy_ips'] );

    return $settings;
  }

  /**
   * Apply a newly selected mode profile without overwriting user changes.
   *
   * @param array $raw      Raw submitted values.
   * @param array $previous Previously saved sanitized settings.
   * @param array $touched  Setting keys explicitly changed in the UI.
   * @return array
   */
  private function apply_mode_profile_to_raw( $raw, $previous, $touched = array() ) {
    $modes         = $this->get_modes();
    $selected_mode = isset( $raw['mode'] ) ? sanitize_key( $raw['mode'] ) : '';

    if ( ! isset( $modes[ $selected_mode ] ) ) {
      $selected_mode = isset( $previous['mode'] ) ? sanitize_key( $previous['mode'] ) : 'balanced';
    }

    $previous_mode = isset( $previous['mode'] ) ? sanitize_key( $previous['mode'] ) : 'balanced';
    if ( $selected_mode === $previous_mode ) {
      return $raw;
    }

    $selected_profile = $this->get_mode_profile( $selected_mode );
    $previous_profile = $this->get_mode_profile( $previous_mode );

    foreach ( $selected_profile as $key => $profile_value ) {
      $previous_value      = array_key_exists( $key, $previous ) ? $previous[ $key ] : null;
      $submitted_value     = array_key_exists( $key, $raw ) ? $raw[ $key ] : null;
      $previous_base_value = array_key_exists( $key, $previous_profile ) ? $previous_profile[ $key ] : null;

      $changed_in_ui         = in_array( $key, $touched, true );
      $changed_in_current_save = array_key_exists( $key, $raw ) && ! $this->setting_values_match( $key, $submitted_value, $previous_value );
      $was_already_custom      = array_key_exists( $key, $previous_profile ) && ! $this->setting_values_match( $key, $previous_value, $previous_base_value );

      if ( $changed_in_ui || $changed_in_current_save || $was_already_custom ) {
        continue;
      }

      $raw[ $key ] = $profile_value;
    }

    return $raw;
  }

  /**
   * Compare two values using each field's expected type.
   *
   * @param string $key   Setting key.
   * @param mixed  $left  First value.
   * @param mixed  $right Second value.
   * @return bool
   */
  private function setting_values_match( $key, $left, $right ) {
    if ( in_array( $key, $this->get_boolean_keys(), true ) ) {
      return (bool) $left === (bool) $right;
    }

    if ( in_array( $key, $this->get_numeric_keys(), true ) ) {
      return (int) $left === (int) $right;
    }

    return (string) $left === (string) $right;
  }

  /**
   * Return checkbox-backed setting keys.
   *
   * @return array
   */
  private function get_boolean_keys() {
    return array(
      'enabled',
      'admin_bypass',
      'log_events',
      'auth_rate_limit_enabled',
      'xmlrpc_protection_enabled',
      'xmlrpc_disable_auth',
      'xmlrpc_disable_multicall',
      'xmlrpc_disable_pingbacks',
      'rest_protection_enabled',
      'rest_rate_limit_enabled',
      'rest_block_anonymous_writes',
      'rest_block_user_enumeration',
      'security_headers_enabled',
      'security_headers_nosniff',
      'security_headers_frame_options',
      'security_headers_referrer',
      'security_headers_permissions',
      'security_headers_csp_frame',
      'app_passwords_guard_enabled',
      'app_passwords_log_usage',
      'app_passwords_admin_only',
      'user_enumeration_guard_enabled',
      'user_enumeration_author_query',
      'request_scoring_enabled',
      'probe_protection_enabled',
      'temp_block_enabled',
      'trusted_proxy_headers_enabled',
    );
  }

  /**
   * Return numeric setting keys.
   *
   * @return array
   */
  private function get_numeric_keys() {
    return array(
      'login_max_attempts',
      'login_ip_max_attempts',
      'lostpassword_max_attempts',
      'register_max_attempts',
      'auth_window_minutes',
      'rest_rate_limit',
      'rest_rate_window_minutes',
      'request_score_threshold',
      'request_score_temp_threshold',
      'probe_strikes',
      'probe_window_minutes',
      'temp_block_minutes',
      'max_url_length',
      'max_query_length',
    );
  }

  /**
   * Sanitize integer field with bounds.
   *
   * @param mixed $value    Submitted value.
   * @param int   $min      Minimum.
   * @param int   $max      Maximum.
   * @param int   $fallback Fallback.
   * @return int
   */
  private function sanitize_int( $value, $min, $max, $fallback ) {
    $value = is_numeric( $value ) ? (int) $value : (int) $fallback;

    return max( $min, min( $max, $value ) );
  }

  /**
   * Sanitize event retention key.
   *
   * @param mixed $value Submitted value.
   * @return string
   */
  private function sanitize_event_retention( $value ) {
    $value   = sanitize_key( (string) $value );
    $choices = array(
      'daily'   => true,
      'weekly'  => true,
      'monthly' => true,
      'yearly'  => true,
    );

    return isset( $choices[ $value ] ) ? $value : 'monthly';
  }

  /**
   * Return supported client IP proxy headers.
   *
   * @return array
   */
  public function get_proxy_header_choices() {
    return array(
      'none'             => __( 'Do not trust proxy headers', 'polanger-admin-menu-manager' ),
      'cf_connecting_ip' => 'CF-Connecting-IP',
      'x_forwarded_for'  => 'X-Forwarded-For',
      'x_real_ip'        => 'X-Real-IP',
    );
  }

  /**
   * Sanitize proxy header selection.
   *
   * @param mixed $value Submitted value.
   * @return string
   */
  private function sanitize_proxy_ip_header( $value ) {
    $value   = sanitize_key( (string) $value );
    $choices = $this->get_proxy_header_choices();

    return isset( $choices[ $value ] ) ? $value : 'none';
  }

  /**
   * Sanitize IP/CIDR textarea into normalized text.
   *
   * @param mixed $raw Raw textarea value.
   * @return string
   */
  private function sanitize_ip_list_text( $raw ) {
    $items = $this->sanitize_ip_list_to_array( $raw );

    return implode( "\n", $items );
  }

  /**
   * Sanitize IP/CIDR textarea into an array.
   *
   * @param mixed $raw Raw textarea value.
   * @return array
   */
  private function sanitize_ip_list_to_array( $raw ) {
    $raw   = is_array( $raw ) ? implode( "\n", $raw ) : (string) $raw;
    $lines = preg_split( '/[\r\n,]+/', $raw );
    $items = array();

    foreach ( (array) $lines as $line ) {
      $line = trim( sanitize_text_field( $line ) );
      if ( '' === $line ) {
        continue;
      }

      if ( false !== strpos( $line, '/' ) ) {
        list( $ip, $bits ) = array_pad( explode( '/', $line, 2 ), 2, '' );
        $ip       = trim( $ip );
        $bits_raw = trim( (string) $bits );
        if ( '' === $bits_raw || ! ctype_digit( $bits_raw ) || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
          continue;
        }

        $bits_int = (int) $bits_raw;
        $max_bits = false !== strpos( $ip, ':' ) ? 128 : 32;
        if ( $bits_int >= 0 && $bits_int <= $max_bits ) {
          $items[] = $ip . '/' . $bits_int;
        }
        continue;
      }

      if ( filter_var( $line, FILTER_VALIDATE_IP ) ) {
        $items[] = $line;
      }
    }

    return array_values( array_unique( $items ) );
  }

  /**
   * Sanitize path prefix textarea into normalized text.
   *
   * @param mixed $raw Raw textarea value.
   * @return string
   */
  private function sanitize_path_prefix_list_text( $raw ) {
    $items = $this->sanitize_path_prefix_list_to_array( $raw );

    return implode( "\n", $items );
  }

  /**
   * Sanitize path prefix textarea into an array.
   *
   * @param mixed $raw Raw textarea value.
   * @return array
   */
  private function sanitize_path_prefix_list_to_array( $raw ) {
    $raw   = is_array( $raw ) ? implode( "\n", $raw ) : (string) $raw;
    $lines = preg_split( '/[\r\n,]+/', $raw );
    $items = array();

    foreach ( (array) $lines as $line ) {
      $line = trim( sanitize_text_field( $line ) );
      if ( '' === $line ) {
        continue;
      }

      $path  = '';
      $query = wp_parse_url( $line, PHP_URL_QUERY );
      if ( is_string( $query ) && '' !== $query ) {
        parse_str( $query, $query_args );
        if ( ! empty( $query_args['rest_route'] ) ) {
          $path = (string) $query_args['rest_route'];
        }
      }

      if ( '' === $path ) {
        $path = wp_parse_url( $line, PHP_URL_PATH );
        $path = is_string( $path ) && '' !== $path ? $path : $line;
      }

      $path      = rawurldecode( $path );
      $site_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
      $site_path = is_string( $site_path ) ? '/' . trim( $site_path, '/' ) . '/' : '/';
      if ( '/' !== $site_path && 0 === strpos( '/' . ltrim( $path, '/' ), $site_path ) ) {
        $path = substr( '/' . ltrim( $path, '/' ), strlen( $site_path ) - 1 );
      }

      $rest_prefix = '/' . trim( rest_get_url_prefix(), '/' ) . '/';
      if ( 0 === strpos( '/' . ltrim( $path, '/' ), $rest_prefix ) ) {
        $path = substr( '/' . ltrim( $path, '/' ), strlen( $rest_prefix ) - 1 );
      }

      $path = '/' . ltrim( $path, '/' );
      $path = preg_replace( '#/+#', '/', $path );

      if ( '/' === $path ) {
        continue;
      }

      $items[] = untrailingslashit( $path ) . '/';
    }

    return array_values( array_unique( $items ) );
  }
}
